JSX-Control-Statements is a Babel plugin that extends JSX to add basic control statements: conditionals and loops.
It does so by transforming component-like control statements to their JavaScript counterparts - e.g. <If condition={condition()}>Hello World!</If>
becomes condition() ? 'Hello World!' : null
.
Developers coming to React from using JavaScript templating libraries like Handlebars are often surprised that there's no built-in looping or conditional syntax. This is by design - JSX by is not a templating library, it's declarative syntactic sugar over functional JavaScript expressions. JSX Control Statements follows the same principle - it provides a component-like syntax that keeps your render
functions neat and readable, but desugars into clean, readable JavaScript.
The only dependency JSX-Control-Statements relies upon is Babel. It is compatible with React and React Native.
It appears to be pretty easy to implement conditionals as React component, which is underlined by the amount of libraries which have taken this approach. However, all of them suffer from the same major caveat: A React component will always evaluate all of its properties including the component body. Hence the following example will fail for those libraries:
<IfComponent condition={ item }>
{ item.title }
</IfComponent>
The error will be "Cannot read property 'title' of undefined", because React will evaluate the body of the custom component and pass it as "children" property to it. The only workaround is to force React into lazy evaluation by wrapping the statement in a function.
This is the reason why conditionals must be implemented in pure JS. JSX-Control-Statements only adds the syntactic sugar to write conditionals as component, while it transforms this "component" to a pure JS expression.
See Alternative Solutions for a more detailed comparison and pure JS solutions.
As a prerequisite you need to have Babel installed and configured in your project.
Install via npm:
npm install --save-dev jsx-control-statements
Then you only need to specify JSX-Control-Statements as Babel plugin, which you would typically do in your .babelrc
.
{
...
"plugins": ["jsx-control-statements"]
}
If you use the transform-react-inline-elements
plugin, place it after jsx-control-statements
:
{
...
"plugins": ["jsx-control-statements", "transform-react-inline-elements"]
}
Babel can be used and configured in many different ways, so use this guide to pick a configuration which fits your setup.
Used to express the most simple conditional logic.
// simple
<If condition={ true }>
<span>IfBlock</span>
</If>
// using multiple child elements and / or expressions
<If condition={ true }>
one
{ "two" }
<span>three</span>
<span>four</span>
</If>
The body of the if statement only gets evaluated if condition
is true.
Prop Name | Prop Type | Required |
---|---|---|
condition | boolean | ✅ |
The else element has no properties and demarcates the else
branch.
This element is deprecated, since it's bad JSX/XML semantics and breaks auto-formatting.
Please use <Choose>
instead.
If statements transform to the ternary operator:
// before transformation
<If condition={ test }>
<span>Truth</span>
</If>
// after transformation
{ test ? <span>Truth</span> : null }
This is an alternative syntax for more complex conditional statements. The syntax itself is XMLish and conforms by and
large to JSTL or XSLT (the attribute is called condition
instead of test
):
<Choose>
<When condition={ test1 }>
<span>IfBlock</span>
</When>
<When condition={ test2 }>
<span>ElseIfBlock</span>
<span>Another ElseIfBlock</span>
<span>...</span>
</When>
<Otherwise>
<span>ElseBlock</span>
</Otherwise>
</Choose>
// default block is optional; minimal example:
<Choose>
<When condition={true}>
<span>IfBlock</span>
</When>
</Choose>
Acts as a simple container and only allows for <When>
and <Otherwise>
as children.
Each <Choose>
statement requires at least one <When>
block but may contain as many as desired.
The <Otherwise>
block is optional.
Analog to <If>
.
Prop Name | Prop Type | Required |
---|---|---|
condition | boolean | ✅ |
<Otherwise>
has no attributes and demarcates the else branch of the conditional.
This syntax desugars into a (sequence of) ternary operator(s).
// Before transformation
<Choose>
<When condition={ test1 }>
<span>IfBlock1</span>
</When>
<When condition={ test2 }>
<span>IfBlock2</span>
</When>
<Otherwise>
<span>ElseBlock</span>
</Otherwise>
</Choose>
// After transformation
{ test1 ? <span>IfBlock1</span> : test2 ? <span>IfBlock2</span> : <span>ElseBlock</span> }
Define <For>
like so:
// you must provide the key attribute yourself
<For each="item" of={ this.props.items }>
<span key={ item.id }>{ item.title }</span>
</For>
// using the index as key attribute is not stable if the array changes
<For each="item" index="idx" of={ [1,2,3] }>
<span key={ idx }>{ item }</span>
<span key={ idx + '_2' }>Static Text</span>
</For>
Prop Name | Prop Type | Required | description |
---|---|---|---|
of | array or collection(Immutable) | ✅ | the array to iterate over. This can also be a collection (Immutable.js) or anything on which a function with the name map can be called |
each | string | a reference to the current item of the array which can be used within the body as variable | |
index | string | a reference to the index of the current item which can be used within the body as variable |
Note that a <For>
cannot be at the root of a render()
function in a React component, because then you'd
potentially have multiple components without a parent to group them which isn't allowed. As with <If>
, the same rules
as using Array.map()
apply - each element inside the loop should have a key
attribute that uniquely identifies it.
There is no implementation for the map function within jsx-control-statements. We only expect that a
function can be called on the passed object (to the of
attribute) which has the same signature as Array.map
.
// before transformation
<For each="item" index="index" of={ items )}>
<span key={ item.id }>{ index }. { item.title }</span>
</For>
// after transformation
{
items.map( function(item, index) {
<span key={ item.id }>{ index }. { item.title }</span>
})
}
Since all control statements are transformed via Babel, no require
or import
calls are needed. This in turn
(well, and some more cases) would lead to warnings or errors by ESLint about undefined variables.
But fortunately you can use this ESLint plugin for JSX-Control-Statements to lint your code.
There's still not a perfect solution for FlowType given that it doesn't provide a lot of plugin functionality
(at least not yet). Flow definitions are available in jsx-control-statements.flow.js
, which will stop the
type checker complaining about statements being undeclared. As of now there's no neat way to make the Flow checker
recognise each
attributes in <For>
loops as a variable - the best workaround for now is something like:
render() {
declare var eachVariable: string;
return (
<For each="eachVariable" of={["hello", "world"]}>
{eachVariable}
</For>
);
}
If you know of a better way to work around this please let us know!
Since everything will be compiled to JavaScript anyway, you might prefer to stick to pure JavaScript solutions.
Probably the most common way for simple conditionals is the use of the && operator:
// simple if
{ test && <span>true</span> }
// additionally the else branch
{ !test && <span>false</span> }
The ternary operator is probably more elegant for if / else conditionals:
// simple
{ test ? <span>true</span> : <span>false</span> }
// with multiple children
{ test ? [<span key="1">one</span>, <span key="2">two</span>] : <span>false</span> }
Another approach is to refactor your conditional into a function:
testFunc(condition){
if(condition) {
return <span>true</span>;
}
else {
return <span>false</span>
}
}
render() {
return (
<div>{ testFunc(test) }</div>
)
}
Not many options here:
{ items.map(function(item) {
<span key={ item.id }>{ item. title }</span>
}) }
Arguments pro JSX-Control-Statements in comparison to pure JS solutions:
- More intuitive and easier to handle for designers and people with non-heavy JS background
- JSX does not get fragmented by JS statements
- Better readability and neatness, but that probably depends on you
Cons:
- Penalty on build-time performance
- Depends on Babel 6
- Some Babel configuration
There are a reasonable amount of React components for conditionals (e.g. react-if, which inspired this in the first place), JSX-Control-Statements is the only approach we know of that avoids execution of all branches (see the intro section), and there seems to be no other component-based solution to looping - while it would be possible to make a component that renders everything in props.children
for every element of an array, you'd have to access the members of the array in that component instead of the one that uses it.
For more discussion on If
in React by the react team, have a look at reactjs/react-future#35.
To sum up:
- Conditionals don't execute invalid paths
- Loops with variable references to each element and index are made possible
- No penalty on runtime performance
- No import / require statements needed to use control statements
- It works exactly as JSX is supposed to work: Plain syntactic sugar
Cons:
- Depends on Babel 6
- Some Babel configuration
- Slightly longer build times
- Requires an extra plugin to work with ESLint
- 3.x.x is a pure Babel plugin supporting Babel >= 6.
- 2.x.x was a Babel plugin supporting Babel >= 6, and a set of JSTransform visitors.
- 1.x.x was a Babel plugin supporting Babel <= 5, and a set of JSTransform visitors.
This used to support both JSTransform and Babel, but as JSTransform is no longer maintained support was dropped. You can find the code for the JSTransform version at https://github.com/AlexGilleran/jsx-control-statements-jstransform.
Yay! Please read the Contributor's Guide.