May 15, 2018
Handling the state of an application is one of the core
features of most frameworks and even react ha a build in
state API. Every class-bases component has its own state
which can be accessed with the class property state
. The
initial value can be set when a new instance of the class is
built. But for every state update you should use the
component method setState
because mutating the state
directly won’t trigger an update in the react component
tree. setState
can either be called with an object
containing the new state properties or with a function which
gets the current state and props as parameters. By calling
it with a function you can be sure to get actual state at
the time of the state update. The simpler call with just a
new state object works most of the times, but with many
state updates in a short time it can come to race conditions
because React can execute multiple state updates before a
new render occurs.
Here is a small hello-state example of storing a number and increment or decrement it with buttons:
class HelloState extends React.Component { constructor() { this.state = { value: 0 }; this.increment = () => { this.setState(state => ({ value: state.value + 1, })); }; this.decrement = () => { this.setState(state => ({ value: state.value - 1, })); }; } render() { const { value } = this.state; return ( <div> <div>value: {value}</div> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> </div> ); } } render(<HelloState />);
One of the most important rules of state management is:
All data in the state should be treated as read only, changes are made by creating new objects and dispatching them as state updates. For objects this means, whenever a property of an object changes you create a new object. And Whenever elements of an array change, you create a new array. Sticking to this rules makes it very easy to determine if a component should update because only the references of the objects must be compared.
Even more important:
Don’t create multiple local states manageing the same data, whenever the state of a component is needed somewhere else leverage it up to the parent component until every component depending on this state is located under the state-holder in the component tree.
This allows you derive multiple different interpretations of a single state and whenever this state gets an updated, every derivation is calculated with the new state.
For most cases the setState
method alone is enough, but
when the state gets a little more complex and state updates
can come from many different parts of the application it is
useful to build a more consistent way to update single parts
of the state. One powerful but simple tool is the
redux-pattern. With redux you
dispatch actions instead of calling setState
directly with
the updated values. Those actions are then digested by
functions called reducers
which create the new state.
const valueReducer = (state, action) => { switch (action.type) { case "INCREMENT": return state + 1; case "DECREMENT": return state - 1; default: return state; } }; const stateReducer = ( state = { value: 0 }, action = {}, ) => { return { value: valueReducer(state.value, action), }; }; const increment = { type: "INCREMENT" }; const decrement = { type: "DECREMENT" }; class HelloRedux extends React.Component { constructor() { this.state = stateReducer(); this.dispatch = action => { this.setState(state => stateReducer(state, action)); }; } render() { const { value } = this.state; return ( <div> <div>value: {value}</div> <button onClick={() => this.dispatch(increment)}> + </button> <button onClick={() => this.dispatch(decrement)}> - </button> </div> ); } } render(<HelloRedux />);
With this pattern it is easy to separate the state management from the UI code and because you are describing the changes with actions instead of direct updates it is easy to extend the model with new properties derived from the already defined actions
const valueReducer = (state, action) => { switch (action.type) { case "INCREMENT": return state + 1; case "DECREMENT": return state - 1; default: return state; } }; const clickedReducer = (state, action) => { switch (action.type) { case "INCREMENT": case "DECREMENT": return state + 1; default: return state; } }; const stateReducer = ( state = { value: 0, clicked: 0 }, action = {}, ) => { return { value: valueReducer(state.value, action), clicked: clickedReducer(state.clicked, action), }; }; const increment = { type: "INCREMENT" }; const decrement = { type: "DECREMENT" }; class HelloRedux extends React.Component { constructor() { this.state = stateReducer(); this.dispatch = action => { this.setState(state => stateReducer(state, action)); }; } render() { const { value, clicked } = this.state; return ( <div> <div>value: {value}</div> <button onClick={() => this.dispatch(increment)}> + </button> <button onClick={() => this.dispatch(decrement)}> - </button> <div>click counter: {clicked}</div> </div> ); } } render(<HelloRedux />);
npm start
) (or
continue with your own design from last session)Written by Kalle Ott for opencampus