├── .github └── workflows │ ├── prettier.yaml │ └── test-and-build.yaml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc.mjs ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.ts ├── eslint.config.mjs ├── jest.config.mjs ├── package.json ├── pnpm-lock.yaml ├── setupTests.ts ├── src ├── components │ ├── else.tsx │ ├── if.tsx │ └── show.tsx ├── index.ts ├── types │ └── polymorphic.ts └── utils │ └── is-fragment.ts ├── test ├── else │ ├── __snapshots__ │ │ └── else.test.tsx.snap │ └── else.test.tsx ├── if │ ├── __snapshots__ │ │ └── if.test.tsx.snap │ └── if.test.tsx └── show │ ├── __snapshots__ │ └── show.test.tsx.snap │ └── show.test.tsx └── tsconfig.json /.github/workflows/prettier.yaml: -------------------------------------------------------------------------------- 1 | name: Prettier 🚀🚀 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | prettier: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.head_ref }} 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v4 25 | with: 26 | version: 9 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: '20' 32 | cache: 'pnpm' 33 | 34 | - name: Install dependencies 35 | run: pnpm install 36 | 37 | - name: Run Prettier 38 | run: pnpm prettier --write . 39 | 40 | - name: Commit changes 41 | uses: stefanzweifel/git-auto-commit-action@v5 42 | with: 43 | commit_message: 'style: format with prettier' 44 | -------------------------------------------------------------------------------- /.github/workflows/test-and-build.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests and Build 🚀🚀 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '20' 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: 9 25 | 26 | - name: Install dependencies 27 | run: pnpm install --frozen-lockfile 28 | 29 | - name: Run tests 30 | run: pnpm test 31 | 32 | - name: Build 33 | run: pnpm build 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm run test && pnpm run build 2 | -------------------------------------------------------------------------------- /.lintstagedrc.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | // Lint and format TypeScript and JavaScript files 3 | '**/*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'], 4 | 5 | // Format other file types 6 | '**/*.{json,md,yml}': ['prettier --write'], 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": true, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[javascriptreact]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.formatOnSave": true 17 | }, 18 | "editor.defaultFormatter": "esbenp.prettier-vscode", 19 | "editor.formatOnSave": true, 20 | "editor.codeActionsOnSave": { 21 | "source.formatDocument": "always", 22 | "source.fixAll.eslint": "always" 23 | }, 24 | "editor.formatOnType": true 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the react-smart-conditional package will be documented in this file. 4 | 5 | ## [1.0.4] - 2025-03-05 6 | 7 | ### Added 8 | 9 | - Added support for React 19. 10 | 11 | ## [1.0.3] - 2025-03-05 12 | 13 | ### Fixed 14 | 15 | - Excluded `ref` and unsupported props when `as` is a `React.Fragment` to prevent React warnings and errors. 16 | 17 | ## [1.0.1] - 2024-09-11 18 | 19 | ### Changes 20 | 21 | - Updated GitHub link in README.md 22 | 23 | ## [1.0.0] - 2024-09-07 24 | 25 | ### Added 26 | 27 | - Initial release of react-smart-conditional 28 | - Created `Show` component in `src/components/show` directory 29 | - Implemented conditional rendering logic 30 | - Added support for basic conditional statements 31 | - Set up project structure and development environment 32 | - Added basic documentation and usage examples 33 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Show Component 2 | 3 | Thank you for your interest in contributing to the Show Component! We welcome contributions from the community to help improve and evolve this React utility. 4 | 5 | ## Getting Started 6 | 7 | 1. Fork the repository on GitHub. 8 | 2. Clone your fork locally. 9 | 3. Ensure you have pnpm installed globally. If not, install it with `npm install -g pnpm`. 10 | 4. Run `pnpm install` to install dependencies. 11 | 5. Create a branch for your contribution. 12 | 13 | ## Development Workflow 14 | 15 | 1. Make your changes in your feature branch. 16 | 2. Add or update tests as necessary. 17 | 3. Ensure all tests pass by running `pnpm test`. 18 | 4. Update documentation in README.md if you've changed functionality. 19 | 5. Run `pnpm run lint` to check for any linting issues. 20 | 21 | ## Pull Request Process 22 | 23 | 1. Ensure your code adheres to the existing style. We use Prettier for formatting. 24 | 2. Update the README.md with details of changes to the interface, if applicable. 25 | 3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. We use SemVer for versioning. 26 | 4. Your Pull Request will be reviewed by maintainers. Be open to feedback and be prepared to make changes if requested. 27 | 28 | ## Reporting Issues 29 | 30 | If you find a bug or have a suggestion for improvement: 31 | 32 | 1. Check if the issue already exists in the GitHub issues. 33 | 2. If not, create a new issue, providing as much relevant information as possible. 34 | 35 | ## Coding Standards 36 | 37 | - Follow React best practices and hooks rules. 38 | - Write clear, readable, and maintainable code. 39 | - Comment your code where necessary, especially for complex logic. 40 | - Write meaningful commit messages. 41 | 42 | ## Testing 43 | 44 | - Add unit tests for any new functionality. 45 | - Ensure all existing tests pass before submitting a Pull Request. 46 | 47 | ## Documentation 48 | 49 | - Update the README.md if you're adding or changing functionality. 50 | - Include JSDoc comments for all functions and components. 51 | 52 | ## Package Management 53 | 54 | We use pnpm for package management. Please make sure to use pnpm commands instead of npm or yarn. 55 | 56 | - To add a new dependency: `pnpm add ` 57 | - To add a dev dependency: `pnpm add -D ` 58 | - To install all dependencies: `pnpm install` 59 | - To run scripts defined in package.json: `pnpm run ` 60 | 61 | Thank you for contributing to react-smart-conditional 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adenuga Oluwatunmise Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/react-smart-conditional?logo=npm)](https://www.npmjs.com/package/react-smart-conditional) 2 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-smart-conditional?label=bundle%20size&logo=webpack)](https://bundlephobia.com/result?p=react-smart-conditional) 3 | [![License](https://img.shields.io/github/license/oluwatunmiisheii/react-smart-conditional?logo=github&logoColor=959DA5&labelColor=2D3339)](https://github.com/oluwatunmiisheii/react-smart-conditional/blob/main/LICENSE) 4 | [![Contact](https://img.shields.io/badge/contact-@__Adenugawilson-blue.svg?style=flat&logo=twitter)](https://x.com/Adenugawilson) 5 | 6 | # React Smart Conditional 7 | 8 | A flexible and reusable React component for conditional rendering. 9 | 10 | ## Features 11 | 12 | - Conditional rendering of content based on boolean conditions 13 | - Support for multiple conditions with `If` components 14 | - Fallback rendering with `Else` component 15 | - Option to render single or multiple true conditions 16 | - Polymorphic component API for flexible element rendering 17 | 18 | ## Table of Contents 19 | 20 | - [Installation](#installation) 21 | - [Usage](#usage) 22 | - [API Reference](#api-reference) 23 | - [Contributing](#contributing) 24 | - [License](#license) 25 | 26 | ## Installation 27 | 28 | ```bash 29 | pnpm install react-smart-conditional 30 | ``` 31 | 32 | ## Usage 33 | 34 | ### Traditional React Conditional Rendering 35 | 36 | The following example demonstrates the traditional way of conditional rendering in React using ternary operators and logical AND operators. While functional, this approach can become difficult to read and maintain as the number of conditions increases. 37 | 38 | ```jsx 39 | import React from 'react'; 40 | 41 | const DataDisplay = ({ isLoading, error, data }) => { 42 | return ( 43 |
44 | {isLoading ? ( 45 |

Loading...

46 | ) : error ? ( 47 |

Error: {error.message}

48 | ) : data ? ( 49 |
50 |

Data Loaded:

51 |
{JSON.stringify(data, null, 2)}
52 |
53 | ) : ( 54 |

No data available.

55 | )} 56 |
57 | ); 58 | }; 59 | 60 | export default DataDisplay; 61 | ``` 62 | 63 | ### Using React Conditional Render 64 | 65 | This example showcases the same component using the `react-smart-conditional` library. The `Show` component and its child components (`If`, and `Else`) provide a more declarative and readable approach to conditional rendering, especially for complex scenarios. 66 | 67 | ```jsx 68 | import React from 'react'; 69 | import { Show } from 'react-smart-conditional'; 70 | 71 | const DataDisplay = ({ isLoading, error, data }) => { 72 | return ( 73 | 74 | Loading... 75 | Error: {error.message} 76 | 77 |

Data Loaded:

78 |
{JSON.stringify(data, null, 2)}
79 |
80 | 81 |

No data available.

82 |
83 |
84 | ); 85 | }; 86 | 87 | export default DataDisplay; 88 | ``` 89 | 90 | ### Rendering Multiple True Conditions 91 | 92 | To render all true conditions, use the `multiple` prop: 93 | 94 | ```jsx 95 | 96 | Content for condition 1 97 | Content for condition 2 98 | Fallback content 99 | 100 | ``` 101 | 102 | This will render both `condition1` and `condition2` if they are true. 103 | 104 | ## API 105 | 106 | 1. **`Show`** - Main container for conditional rendering 107 | 108 | - Props: 109 | - `multiple`: boolean (default: false) - When true, renders all true conditions. When false, renders only the first true condition. 110 | - `as?: string | React.ComponentType` - Wrapper element/component (optional, default: React.Fragment) 111 | - `children: React.ReactNode` - Should contain `If`, `ElseIf`, and `Else` components. 112 | 113 | 2. **`Show.If`** - Renders children when condition is true 114 | 115 | - Props: 116 | - `as?: string | React.ComponentType` - Wrapper element/component (optional, default: div) 117 | - `condition: boolean` - Condition to evaluate (required) 118 | - `children: React.ReactNode` - Content to render if true 119 | 120 | 3. **`Show.Else`** - Renders when all previous conditions were false 121 | - Props: 122 | - `as?: string | React.ComponentType` - Wrapper element/component (optional, default: div) 123 | - `children: React.ReactNode` - Content to render 124 | 125 | ## Polymorphic API 126 | 127 | The `Show`, `Show.If`, and `Show.Else` components support polymorphic rendering: 128 | 129 | ```jsx 130 | 131 | console.log('Clicked')} 136 | > 137 | Paragraph content 138 | 139 | 145 | Hidden content 146 | 147 | 148 | Span content 149 | 150 | 151 | ``` 152 | 153 | ## Contributing 154 | 155 | Contributions are welcome! Please feel free to submit a Pull Request. 156 | 157 | 1. Fork the repository 158 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 159 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 160 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 161 | 5. Open a Pull Request 162 | 163 | If you find this project helpful, please consider giving it a star on GitHub! ⭐️ 164 | 165 | ## License 166 | 167 | Distributed under the MIT License. See `LICENSE` for more information. 168 | 169 | ## Contact 170 | 171 | Made with ❤️ Wilson Adenuga - [@Adenugawilson](https://x.com/Adenugawilson) - oluwatunmiseadenuga@gmail.com 172 | 173 | Project Link: [https://github.com/oluwatunmiisheii/react-smart-conditional](https://github.com/oluwatunmiisheii/react-smart-conditional) 174 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import { RuleConfigSeverity } from '@commitlint/types'; 2 | 3 | const config = { 4 | extends: ['@commitlint/config-conventional'], 5 | 6 | rules: { 7 | 'type-enum': [ 8 | RuleConfigSeverity.Error, 9 | 'always', 10 | [ 11 | 'feat', 12 | 'fix', 13 | 'docs', 14 | 'style', 15 | 'refactor', 16 | 'perf', 17 | 'test', 18 | 'build', 19 | 'ci', 20 | 'chore', 21 | 'revert', 22 | 'wip', 23 | ], 24 | ], 25 | }, 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import pluginReact from 'eslint-plugin-react'; 4 | 5 | export default [ 6 | { files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] }, 7 | { 8 | languageOptions: { 9 | globals: { 10 | React: true, 11 | ReactDOM: true, 12 | jest: true, 13 | browser: true, 14 | node: true, 15 | }, 16 | }, 17 | }, 18 | pluginJs.configs.recommended, 19 | ...tseslint.configs.recommended, 20 | pluginReact.configs.flat.recommended, 21 | { 22 | languageOptions: { 23 | parserOptions: { 24 | sourceType: 'module', 25 | ecmaVersion: 2021, 26 | ecmaFeatures: { 27 | jsx: true, 28 | }, 29 | }, 30 | }, 31 | rules: { 32 | '@typescript-eslint/no-non-null-assertion': 'off', 33 | '@typescript-eslint/ban-ts-comment': 'off', 34 | '@typescript-eslint/no-explicit-any': 'off', 35 | }, 36 | settings: { 37 | react: { 38 | pragma: 'React', 39 | version: 'detect', 40 | }, 41 | }, 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | roots: ['/test'], 5 | collectCoverage: true, 6 | collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!**/vendor/**'], 7 | coverageDirectory: 'coverage', 8 | coveragePathIgnorePatterns: [ 9 | '/node_modules/', 10 | '/coverage', 11 | 'package.json', 12 | 'reportWebVitals.ts', 13 | 'setupTests.ts', 14 | 'index.tsx', 15 | ], 16 | setupFilesAfterEnv: ['/setupTests.ts'], 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-smart-conditional", 3 | "description": "Manage conditional rendering in react js and it's frameworks like a pro", 4 | "version": "1.0.4", 5 | "source": "src/index.ts", 6 | "main": "dist/index.js", 7 | "module": "dist/index.module.js", 8 | "esmodule": "dist/index.mjs", 9 | "umd:main": "dist/index.umd.js", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "module": "./dist/index.module.js", 15 | "import": "./dist/index.mjs", 16 | "require": "./dist/index.js" 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "sideEffects": false, 21 | "scripts": { 22 | "lint": "eslint '**/*.{js,ts,tsx}' --ignore-pattern 'dist/'", 23 | "lint:fix": "pnpm lint --fix", 24 | "prettier": "prettier --write \"{src,tests,example/src}/**/*.{js,ts,jsx,tsx}\"", 25 | "build-only": "rm -rf ./dist/*; microbundle build --entry src/index.ts --name react-smart-conditional --tsconfig tsconfig.json", 26 | "build": "pnpm run build-only && size-limit", 27 | "test": "jest", 28 | "prepublishOnly": "pnpm run test && pnpm run build", 29 | "prepare": "husky" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "keywords": [ 35 | "react", 36 | "conditional", 37 | "render", 38 | "conditional-render", 39 | "react-conditional-render" 40 | ], 41 | "author": "Wilson Adenuga ", 42 | "license": "MIT", 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/oluwatunmiisheii/react-smart-conditional.git" 46 | }, 47 | "homepage": "https://github.com/oluwatunmiisheii/react-smart-conditional#readme", 48 | "bugs": { 49 | "url": "https://github.com/oluwatunmiisheii/react-smart-conditional/issues" 50 | }, 51 | "devDependencies": { 52 | "@commitlint/cli": "^19.4.1", 53 | "@commitlint/config-conventional": "^19.4.1", 54 | "@commitlint/types": "^19.0.3", 55 | "@size-limit/preset-small-lib": "^11.1.4", 56 | "@testing-library/dom": "^10.4.0", 57 | "@testing-library/jest-dom": "^6.5.0", 58 | "@testing-library/react": "^16.0.1", 59 | "@types/jest": "^29.5.12", 60 | "@types/react": "^18.3.5", 61 | "eslint": "^9.10.0", 62 | "eslint-config-prettier": "^9.1.0", 63 | "eslint-plugin-prettier": "^5.2.1", 64 | "eslint-plugin-react": "^7.35.2", 65 | "husky": "^9.1.5", 66 | "jest": "^29.7.0", 67 | "jest-environment-jsdom": "^29.7.0", 68 | "lint-staged": "^15.2.8", 69 | "microbundle": "^0.15.1", 70 | "prettier": "^3.3.3", 71 | "react": "^18.3.1", 72 | "react-dom": "^18.3.1", 73 | "size-limit": "^11.1.4", 74 | "ts-jest": "^29.2.5", 75 | "typescript": "^5.5.4", 76 | "typescript-eslint": "^8.4.0" 77 | }, 78 | "peerDependencies": { 79 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 80 | }, 81 | "size-limit": [ 82 | { 83 | "path": "dist/index.js", 84 | "limit": "1 KB" 85 | }, 86 | { 87 | "path": "dist/index.module.js", 88 | "limit": "1 KB" 89 | }, 90 | { 91 | "path": "dist/index.umd.js", 92 | "limit": "1 KB" 93 | }, 94 | { 95 | "path": "dist/index.mjs", 96 | "limit": "1 KB" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /src/components/else.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { polymorphicForwardRef } from '../types/polymorphic'; 3 | import { isFragment } from '../utils/is-fragment'; 4 | 5 | export const Else = polymorphicForwardRef<'div', JSX.IntrinsicElements['div']>( 6 | ({ as: Element = 'div', ...props }, ref) => { 7 | return isFragment(Element) ? ( 8 | props.children 9 | ) : ( 10 | 11 | ); 12 | }, 13 | ); 14 | Else.displayName = 'Else'; 15 | -------------------------------------------------------------------------------- /src/components/if.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { polymorphicForwardRef } from '../types/polymorphic'; 3 | import { isFragment } from '../utils/is-fragment'; 4 | 5 | export const If = polymorphicForwardRef< 6 | 'div', 7 | JSX.IntrinsicElements['div'] & { condition: boolean } 8 | >(({ as: Element = 'div', condition, ...props }, ref) => 9 | condition ? ( 10 | isFragment(Element) ? ( 11 | props.children 12 | ) : ( 13 | 14 | ) 15 | ) : null, 16 | ); 17 | If.displayName = 'If'; 18 | -------------------------------------------------------------------------------- /src/components/show.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode, Children, isValidElement } from 'react'; 2 | import { If } from './if'; 3 | import { Else } from './else'; 4 | import { polymorphicForwardRef } from '../types/polymorphic'; 5 | import { isFragment } from '../utils/is-fragment'; 6 | 7 | type ConditionalComponent = typeof If | typeof Else; 8 | 9 | type ShowProps = { 10 | multiple?: boolean; 11 | }; 12 | 13 | const Show = polymorphicForwardRef< 14 | 'div', 15 | JSX.IntrinsicElements['div'] & ShowProps 16 | >(({ as: Element = 'div', children, multiple = false, ...props }, ref) => { 17 | const trueConditions: ReactNode[] = []; 18 | let Otherwise: ReactNode = null; 19 | 20 | Children.toArray(children).forEach((child) => { 21 | if (isValidElement<{ condition?: boolean }>(child)) { 22 | const childType = child.type as ConditionalComponent; 23 | if (childType === If && child.props.condition) { 24 | trueConditions.push(child); 25 | if (!multiple) return; 26 | } else if (childType === Else && !Otherwise) { 27 | Otherwise = child; 28 | } 29 | } else { 30 | console.warn('Invalid child type in Show component'); 31 | } 32 | }); 33 | 34 | const content = 35 | trueConditions.length > 0 36 | ? multiple 37 | ? trueConditions 38 | : trueConditions[0] 39 | : Otherwise; 40 | 41 | return isFragment(Element) ? ( 42 | content 43 | ) : ( 44 | 45 | {content} 46 | 47 | ); 48 | }); 49 | Show.displayName = 'Show'; 50 | 51 | const ShowWithComponents = Object.assign(Show, { 52 | If, 53 | Else, 54 | }); 55 | export default ShowWithComponents; 56 | export { ShowWithComponents as Show }; 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Show } from './components/show'; 2 | -------------------------------------------------------------------------------- /src/types/polymorphic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | forwardRef, 3 | type ComponentPropsWithRef, 4 | type ElementType, 5 | type ForwardRefExoticComponent, 6 | type ForwardRefRenderFunction, 7 | type ReactElement, 8 | } from 'react'; 9 | 10 | type DistributiveOmit = T extends any 11 | ? Omit 12 | : never; 13 | 14 | type Merge = Omit & B; 15 | type DistributiveMerge = DistributiveOmit & B; 16 | 17 | export type AsProps< 18 | Component extends ElementType, 19 | PermanentProps extends object, 20 | ComponentProps extends object, 21 | > = DistributiveMerge; 22 | 23 | export type PolymorphicWithRef< 24 | Default extends OnlyAs, 25 | Props extends object = object, 26 | OnlyAs extends ElementType = ElementType, 27 | > = ( 28 | props: AsProps>, 29 | ) => ReactElement | null; 30 | 31 | export type PolyForwardComponent< 32 | Default extends OnlyAs, 33 | Props extends object = object, 34 | OnlyAs extends ElementType = ElementType, 35 | > = Merge< 36 | ForwardRefExoticComponent< 37 | Merge, Props & { as?: Default }> 38 | >, 39 | PolymorphicWithRef 40 | >; 41 | 42 | export type PolyRefFunction = < 43 | Default extends OnlyAs, 44 | Props extends object = object, 45 | OnlyAs extends ElementType = ElementType, 46 | >( 47 | Component: ForwardRefRenderFunction, 48 | ) => PolyForwardComponent; 49 | 50 | export const polymorphicForwardRef = forwardRef as PolyRefFunction; 51 | -------------------------------------------------------------------------------- /src/utils/is-fragment.ts: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | 3 | export const isFragment = (Component: React.ElementType) => 4 | Component === Fragment; 5 | -------------------------------------------------------------------------------- /test/else/__snapshots__/else.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Else should not pass ref and props when custom component is a Fragment 1`] = ` 4 |
5 | Hello 6 |
7 | `; 8 | 9 | exports[`Else should pass additional props 1`] = ` 10 |
11 |
15 | Hello 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /test/else/else.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Else } from '../../src/components/else'; 3 | import { render, screen } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | 6 | describe('Else', () => { 7 | it('should render as expected', () => { 8 | render(Hello); 9 | expect(screen.queryByText('Hello')).toBeInTheDocument(); 10 | }); 11 | 12 | it('should render correctly with a different element type', () => { 13 | const { container } = render(Hello); 14 | expect(screen.queryByText('Hello')).toBeInTheDocument(); 15 | expect(container.firstChild?.nodeName).toBe('SPAN'); 16 | }); 17 | 18 | it('should forward ref', () => { 19 | const ref = React.createRef(); 20 | render(Hello); 21 | expect(ref.current).toBeInTheDocument(); 22 | }); 23 | 24 | it('should pass additional props', () => { 25 | const { container } = render( 26 | 27 | Hello 28 | , 29 | ); 30 | expect(container).toMatchSnapshot(); 31 | }); 32 | 33 | it('should not pass ref and props when custom component is a Fragment', () => { 34 | const ref = React.createRef(); 35 | 36 | const { container } = render( 37 | 38 | Hello 39 | , 40 | ); 41 | expect(ref.current).toBeNull(); 42 | expect(container).toMatchSnapshot(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/if/__snapshots__/if.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`If should not pass ref and props when custom component is a Fragment 1`] = ` 4 |
5 | Hello 6 |
7 | `; 8 | 9 | exports[`If should pass additional props 1`] = ` 10 |
11 |
15 | Hello 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /test/if/if.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | import { If } from '../../src/components/if'; 4 | import { render, screen } from '@testing-library/react'; 5 | 6 | describe('If', () => { 7 | it.each([ 8 | [true, true], 9 | [false, false], 10 | ])( 11 | 'should render children when condition is %s', 12 | (condition, shouldRender) => { 13 | render(Hello); 14 | if (shouldRender) { 15 | expect(screen.queryByText('Hello')).toBeInTheDocument(); 16 | } else { 17 | expect(screen.queryByText('Hello')).not.toBeInTheDocument(); 18 | } 19 | }, 20 | ); 21 | 22 | it('should render correctly with a custom element type', () => { 23 | const { container } = render( 24 | 25 | Hello 26 | , 27 | ); 28 | expect(screen.queryByText('Hello')).toBeInTheDocument(); 29 | expect(container.firstChild?.nodeName).toBe('SPAN'); 30 | }); 31 | 32 | it('should forward ref', () => { 33 | const ref = React.createRef(); 34 | render( 35 | 36 | Hello 37 | , 38 | ); 39 | expect(ref.current).toBeInTheDocument(); 40 | }); 41 | 42 | it('should pass additional props', () => { 43 | const { container } = render( 44 | 45 | Hello 46 | , 47 | ); 48 | expect(container).toMatchSnapshot(); 49 | }); 50 | 51 | it('should not pass ref and props when custom component is a Fragment', () => { 52 | const ref = React.createRef(); 53 | 54 | const { container } = render( 55 | 62 | Hello 63 | , 64 | ); 65 | expect(ref.current).toBeNull(); 66 | expect(container).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/show/__snapshots__/show.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Show component polymorphic does not pass ref and props when as is a Fragment 1`] = ` 4 |
5 |
6 | If content 7 |
8 |
9 | `; 10 | -------------------------------------------------------------------------------- /test/show/show.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Show } from '../../src/components/show'; 3 | import { render, screen } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | 6 | describe('Show component', () => { 7 | it('renders first true If child when multiple is false (default)', () => { 8 | render( 9 | 10 | If content 1 11 | If content 2 12 | Else content 13 | , 14 | ); 15 | expect(screen.getByText('If content 1')).toBeInTheDocument(); 16 | expect(screen.queryByText('If content 2')).not.toBeInTheDocument(); 17 | expect(screen.queryByText('Else content')).not.toBeInTheDocument(); 18 | }); 19 | 20 | it('renders all true If children when multiple is true', () => { 21 | render( 22 | 23 | If content 1 24 | If content 2 25 | Else content 26 | , 27 | ); 28 | expect(screen.getByText('If content 1')).toBeInTheDocument(); 29 | expect(screen.getByText('If content 2')).toBeInTheDocument(); 30 | expect(screen.queryByText('Else content')).not.toBeInTheDocument(); 31 | }); 32 | 33 | it('renders Else child when no If conditions are true', () => { 34 | render( 35 | 36 | If content 1 37 | If content 2 38 | Else content 39 | , 40 | ); 41 | expect(screen.queryByText('If content 1')).not.toBeInTheDocument(); 42 | expect(screen.queryByText('If content 2')).not.toBeInTheDocument(); 43 | expect(screen.getByText('Else content')).toBeInTheDocument(); 44 | }); 45 | 46 | it('renders nothing when no conditions are true and no Else is provided', () => { 47 | const { container } = render( 48 | 49 | If content 1 50 | If content 2 51 | , 52 | ); 53 | expect(container.firstChild).toBeEmptyDOMElement(); 54 | }); 55 | 56 | it('warns about invalid child types', () => { 57 | const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); 58 | render( 59 | 60 | Valid child 61 | {/* @ts-ignore - Intentionally passing an invalid child */} 62 | {5} 63 | , 64 | ); 65 | expect(consoleSpy).toHaveBeenCalledWith( 66 | 'Invalid child type in Show component', 67 | ); 68 | consoleSpy.mockRestore(); 69 | }); 70 | 71 | describe('polymorphic', () => { 72 | it.each<[string, React.ElementType, boolean | undefined]>([ 73 | ['Show.If', Show.If, true], 74 | ['Show.Else', Show.Else, undefined], 75 | ])('renders %s as a default element', (name, Component, condition) => { 76 | const { container } = render( 77 | 78 | 79 | {name} content 80 | 81 | , 82 | ); 83 | expect(container.firstChild?.firstChild?.nodeName).toBe('DIV'); 84 | }); 85 | 86 | it.each<[string, React.ElementType, boolean | undefined]>([ 87 | ['Show.If', Show.If, true], 88 | ['Show.Else', Show.Else, undefined], 89 | ])('renders %s as a custom element', (name, Component, condition) => { 90 | const { container } = render( 91 | 92 | 96 | {name} content 97 | 98 | , 99 | ); 100 | expect(container.firstChild?.firstChild?.nodeName).toBe('SPAN'); 101 | }); 102 | 103 | it.each<[string, React.ElementType, boolean | undefined]>([ 104 | ['Show.If', Show.If, true], 105 | ['Show.Else', Show.Else, undefined], 106 | ])( 107 | 'passes additional props and forwards ref', 108 | (name, Component, condition) => { 109 | const ref = React.createRef(); 110 | const { container } = render( 111 | 112 | 119 | {name} content 120 | 121 | , 122 | ); 123 | const button = container.firstChild?.firstChild as HTMLButtonElement; 124 | expect(button.nodeName).toBe('BUTTON'); 125 | expect(button.type).toBe('submit'); 126 | expect(button.className).toBe('custom-class'); 127 | expect(ref.current).toBe(button); 128 | }, 129 | ); 130 | 131 | it('does not pass ref and props when as is a Fragment', () => { 132 | const ref = React.createRef(); 133 | 134 | const { container } = render( 135 | 136 | If content 137 | , 138 | ); 139 | expect(ref.current).toBeNull(); 140 | expect(container).toMatchSnapshot(); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "jsxFactory": "React.createElement", 10 | "lib": ["es5", "es2015", "dom"], 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "outDir": "./dist", 18 | "rootDir": "./src", 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "target": "ESNext" 23 | }, 24 | "include": ["src/index.ts"], 25 | "exclude": ["node_modules", "test", "examples", "dist"] 26 | } 27 | --------------------------------------------------------------------------------