Lately I've been trying to write my React components as "Pure Functions" and I've noticed that sometimes I want to have something which feels a lot like state
. I was thinking about passing my state
as a second parameter to my component. I can achieve this by calling my component as a normal function with two parameters, props
and state
.
For example:
// abstracted to it's own module
const useState = (Component, state = {}) => {
return class extends React.Component {
state = createState(this, state); // will traverse and update the state
render() {
const { props, state } = this;
return Component(props, state); // <-- call the Component directly
}
};
};
const Component = (props, { index, increase }) => (
<div onClick={increase} {...props}>
Click me to increase: {index}
</div>
);
const componentState = {
index: 0,
increase: (event, state) => ({ ...state, index: state.index + 1 })
};
const StatefullComponent = useState(Component, componentState);
<StatefullComponent style={{ color: "purple" }} />;
I have a CodeSandbox example:
My questions are:
shouldComponentUpdate
? (I have a sinking feeling this is modelling the old context
api)Note: I'm using state
in this example, but it could also be a theme
, authorisation rules or other things you might want passed into your component.
At first glanced when I checked your code I had a question:
"Why do you make it so complicated? When you can simply make it with a class declaration".
But later when I have splitted your code I found it really worth to do that.
Question 1: Doesn't really make a difference, it is the way how HOC does the composition.
I'm no longer extending the props with state values, this might be a good thing
Why/When might it be a good thing?
I am messing with the way components are rendered by default, this might be a bad thing
I don't see that you break or mess the rendering by default, I think the HOC pattern promotes the same philosophy, the difference you separate state from props.
Question 2: If a developer decide to use a stateless component then he/she should realize all “lifecycle methods” or references ref
will be not available.
Your pattern make stateless component as “statefull” but in stateless declaration - amazing 😋.
Like in JSX
you write in JS an "HTML" and inside it JS code with another "HTML":
<ul>
{list.map(text => <li>text</li>)} // I know there should be used key
</ul>
Mr. Baudin
pattern (state-full like stateless):
import React from 'react'
import {useState} from './lib'
const state = {
index: 0,
increase: (event, state) => ({index: state.index + 1})
}
const Component = (props, state) => (
<div onClick={state.increase} {...props}>
Click me to increase: {state.index}
</div>
)
export default useState(Component, state)
Question 3: It depends what break changes will be in coming versions.
Question 4: Well... I don't think the offered pattern (implemented library) can be considered as application state management but it can be used within any state management like Redux or Mobx because it deals with internal component state.
Question 5: No, I don't think. Your solution makes code less and clean. Functional components are good for very simple or representational components and now it can be extended with state.
While this question has been open I've done some painful research on the subject and I'd like to share this research with you.
Question 1: Performance; Calling your components as functions or even as constructor functions doesn't really make a difference. You simply get your component instead of a type.
// the component
const MyComponent = () => (<div>This is my page</div>);
console.log(MyComponent());
console.log(new MyComponent());
console.log(<MyComponent />);
console.log(React.createElement(MyComponent));
Pen (Don't forget to inspect the developer tools!)
What I've noticed is that when you call a component directly you lose a little information, for example, when I use JSX the type information is preserved:
React.createElement(MyComponent).type === MyComponent // <- true
MyComponent() // <- Now way to find out what constructed this...
This doesn't seem like a big deal because the MyComponent()
is seen as a normal div
so it should render correctly; but I can imagine that React might do some lookup on the type of the component and calling your function like this that might interfere with the performance.
Haven't found anything in the documentation nor in the source code to suggest that this is the case, so I see no reason to worry about performance at this point.
Question 2: Does this break shouldComponentUpdate
; the answer is "maybe not", but not because I need to write a class
as was suggested. The problem is that React does a shallow compare on the props
when you use a PureComponent
and with pure functions just expects that with the same props you get the same result. In my case, because of the second parameter it might think the component doesn't need to update but actually it should. Because of some magic in my implementation this seems to work for child components of a root component wrapped with the useState
function.
This is as I expected the same problem as with the original implementation of the context
api. And as such I should be able to solve it using some reactive techniques.
Question 3: Seeing how "just calling a component as a function" seems to be the entire idea behind react and seeing how it results in almost exactly the same component without the original type information I see no reason why this should break in the future.
Question 4/5: No, there is no more "Reacty" way of really solving this problem. There is how ever a more functional way. I could use a state monad and lift the entire thing up; but that would envolve a lot of work and I really can't see the benefit of doing that. Passing state as a second parameter seems, at least for now, as something which might be strange but viable and actually feasable.
Question 5: When I started looking around I didn't find a lot os answers to these questions, but now that I've really dug myself in I can see a few other libraries doing the same thing. For example: recompose which calls itself "lodash for react". They seem to use this pattern of wrapping your component in a function and returning a class a lot. (Their withState implementation).
Extra information: My conclusion is that this pattern (because it's nothing more than a pattern) is valid and does not break any fundamental rules of React. Just to give a little bit of extra information Bernardo Ferreira Bastos Braga wrote that I needed to use a class
to do it "the React way". I fail to see how wrapping your function and returning a class with state is anything other than "using a class".
I do however realise that wrapping a function increases complexity, but not by much; function calls are really optimised and because you write for maintainability and optimise later.
One of my biggest fears is that when the software gets more and more complocated and we get more cross-cutting concerns to deal with, it will get harder and harder to handle every concern as a parameter. In this case it might be good to use a destructuring pattern to get the concerns which you need from a "concerns" obejct passed as the second parameter.
One last thing about this pattern. I've done a small test (Just selenium rendering a page a 100 times) and this pattern, on a small scale, is faster than using Redux. The bigger your redux state gets and the more components you connect the faster this pattern becomes. The down side is that you are now doing a bit of manual state management, this comes with a real cost in complexity. Just remember to weigh all options.
Applications which interact with users, require that you try to keep track of the interactions they have. These interactions can be modeled in different ways but I really like a stateful approach. This means that you 'thread' state through your application. Now in react you have a few ways of creating components. The three I want o mention are:
Component
PureComponent
I really like the last option but, to be honest, it's a pain keeping your code performant. There are a lot of articles our there explaining how lambda expression will create a new function every time your component is called, breaking the shallow compare of the props
done by PureComponent
.
To counteract this I use a pattern where I wrap my stateless component in a HoC where I pass my component and my state object. This HoC does some magic and passes the state as a second parameter to the stateless function, ensuring that when the props are tested by the compare of the PureComponent
it should work.
Now to make the wrapper even better I memoize the lambdas so that only a single reference to that function exists so that even if you were to test the function by reference it should still be OK.
The code I use for this is:
return Object.entries(_state).reduce(
(acc, entry) => {
const [key, value] = entry;
if (value instanceof Function) {
acc[key] = _.memoize(item => (...args) => {
const newState = value.apply(null, [...args, root.state, root.props]);
root.setState(newState);
});
} else {
acc[key] = value;
}
return acc;
},
{}
);
};
As you can see I memoize the function and call it proxying the arguments and passing in the state and the props. This works as long as you can call these functions with a unique object like so:
const MyComponent = useState((props, { items, setTitle }) => {
return (
<div>
{items.map(item => (
<Component key={item.id} item={item} changeItem={setTitle(item)} />
))}
</div>
);
}, state);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With