An experiment in making a functional API for React components
This is an experimental library to see what it would be like to code React components in a completely functional way, inspired by Deku.
To define a React component in functional-react, you define your render and lifecycle methods as pure functions (without using a this
pointer) and then pass them to functional-react, which returns a React component. In the simplest case, a component that reads out a single string looks like this:
import component from "functional-react";
const render = ({props}) => {
return <div>Hello, {props.name}!</div>;
}
export default component({render});
Lifecycle methods can be used, too:
import component from "functional-react";
const shouldComponentUpdate = ({props}, {props: nextProps}) => {
// if the name hasn't changed, there's no reason to update.
return props.name !== nextProps.name;
}
const render = ({props}) => {
return <div>Hello, {props.name}!</div>;
}
export default component({render, shouldComponentUpdate});
One wrinkle occurs when you want to render a component with an event handler, as binding the event handler is problematic; there is no this
pointer to bind to at render time. For this, there is a second argument passed to render
which should wrap all event handlers; it makes sure that the event handlers receive the correct props, state, and setState.
import component from "functional-react";
const alertMyName = ({props}, setState) => {
alert(`Hey there, ${props.name}!`);
}
const render = ({props}, handler) => {
// note that we wrap alertMyName so that it gets bound to the correct
// component props & state.
return <div onClick={handler(alertMyName)}>Hello, {props.name}!</div>;
}
export default component({render});
The Component API maps very directly onto the React component API. All of the existing React "instance" methods are passed in a simple object with the current props and state as the first argument, so they use that rather than using this.props
or this.state
. In methods where it would be appropriate to call this.setState()
, that method is passed in as well.
When setState
is passed in, it is a function with the same signature and behavior as React's setState
.
The first argument is the current props & state, which should be all that the component needs to render. Note that the second argument is a function that will wrap an event handler function and return a handler that is always bound correctly to the correct component props & state. See the Event API below for event handler signatures.
Returns the initial state of an instance of this component. This method is not passed props & state, as the component has not yet been created.
Returns the default set of props for an instance of this component; note that the default props are shared amongst all instances.
Used for validating props that are passed to this component.
This is just passed through to React as the component's displayName
.
Called before the component is mounted into the DOM, with the current props & state combo and the setState
function.
Called after the component is mounted into the DOM, with the current props & state combo. Note that setState
is not passed in, as it would not do anything meaningful to the component post-mounting.
The first argument is the current props & state, the second argument is the new props that the component will have, and the third argument is the setState
function that can change this component's state if necessary.
The first argument is the current props & state, and the second arg is the next props & state. Returns a boolean indicating whether React should re-render this component.
The first argument is the current props & state, and the second arg is the next props & state. Note that setState
is not sent to this function, as the React documentation states: "You cannot use this.setState()
in this method. If you need to update state in response to a prop change, use componentWillReceiveProps
instead."
The first argument is the current props & state, and the second arg is the previous props & state. Note that, unlike componentWillUpdate
, the second argument is the earlier props & state; this is because I decided to standardize on the idea that current props & state is always the first argument.
Called before the component is unmounted from the DOM.
When an event handler is passed to handler
in the render
method, it guarantees that the event handler will always be called with the following arguments:
The first argument is the current props & state, the second argument is the setState function that can change this component's state if necessary.
I'm not really sure. There's a certain purity to it, and it helps developers make mistakes in the lifecycle, for two reasons that I can see:
- Components can't messily put attributes on the
this
object and expect them to be around during later lifecycle calls. As a result, components have to be more rigorous about using props and state. - It's somewhat clearer that props and state can't be changed in place. Some developers try to change
this.props
orthis.state
directly, and that causes nothing but sadness. - The API is clearer about when
setState
is allowed to be called and when it isn't. In React, developers can sometimes shoot themselves in the foot callingthis.setState
in the wrong method.
Those seem better, especially for new developers, but frankly, they seem like just marginal wins to me.
One thing that I haven't fully groked is what this does to mixins. I think that in many cases mixins just become functions that you import. For example, imagine a component whose render only depends on props and state and therefore wants to implement a "pure" shouldComponentUpdate
function:
import component from "functional-react";
import {PureShouldComponentUpdate} from "some-should-component-update-lib-of-your-choosing";
const render = ({props}) => {
return <div>Hello, {props.name}!</div>;
}
export default component({render, shouldComponentUpdate:PureShouldComponentUpdate});
You could also imagine composing library functions together into chains to implement a lifecycle method for your component. This seems to me a bit cleaner than the current mixin story, but I doubt I've considered all its contours.