If you had to work with Jquery to manipulate DOM elements and having to infer variables from the DOM this will be a very welcome change.
State management is a way of storing all of your data in one place so that it is always a reflection of the correct data even as it changes.
It simplifies debugging errors in the application because you are able to look at the data itself separate from the DOM and can break down if the issue is with the data itself or the manipulation of the DOM from the data.
Enter Redux to be our state management tool. A couple of key features of this tool are:
- Single source of truth – The data here is always assumed to be accurate and the latest data.
- Data in the store is immutable – It can only be updated and not changed
The gif below explains how our state management work
gif taken from https://dev.to/oahehc/redux-data-flow-and-react-component-life-cycle-11n
At first glance, there is a lot going on there so lets break down what is happening. We’ll run through an example project using the Rick and Morty API to load characters on page load
On page load (using the useEffect hook), getCharacters is dispatched from the CharactersContainer.
useEffect(() => { dispatch(getCharacters()) }, [])
This dispatches the getCharacters ActionType to call the character reducer.
This passes GET_CHARACTERS_BEGIN to the reducer and we use the spread operator to return a new state object with loading set to true
case GET_CHARACTERS_BEGIN: return { ...state, isLoading: true, }
The CharacterContainers props get updated from the store and we use conditional rendering to hide the Characters component and show the Loading component
<div className={classes.bottomContainerPadding}> { !props.isLoading && <Characters characters={props.characters} /> } { props.isLoading && <Loading /> } </div>
Which results with the loading screen being rendered
We use middleware (redux saga – we’ll get to that in a later post) to make the api and call GET_CHARACTERS_SUCCESS with resulting characters. Using this information, we update our isLoading state to false, and set our characters array with the information
case GET_CHARACTERS_SUCCESS: return { ...state, isLoading: false, characters: action.payload.results }
The updated state gets passed to the characters container through props and we use conditional rendering to hide the loading component and show the character component
One of biggest complaints when first working with these projects is figuring out the flow of things to add when a new action needs to be added to the store. So I will show step by step how to setup the data store and add an action.
For the first time setup we need to define the data store:
export interface AppState { characterStateReducer : ICharactersState, applicationSettingsStateReducer : IApplicationSettingsState, router : History }
Then we create the data store:
const rootReducer = (history : History) => combineReducers({ characterStateReducer, applicationSettingsStateReducer, router: connectRouter(history) }) const store = createStore(rootReducer(history), initialState, applyMiddleware(logger, routerMw))
Whenever we create a new container it needs to be hooked up to the store like the following:
const MapStateToProps = (state: AppState) => ({ characters: state.characterStateReducer.characters, isLoading: state.characterStateReducer.isLoading }); const MapDispatchToProps = (dispatch: Dispatch<ActionTypes>) => ({ getCharacters: () => dispatch({ type: GET_CHARACTERS_BEGIN, payload: undefined }), }); export default connect( MapStateToProps, MapDispatchToProps )(CharactersContainer)
Add the action type(s) in the reducer
export const GET_CHARACTERS_BEGIN = 'GET_CHARACTERS_BEGIN' export const GET_CHARACTERS_SUCCESS = 'GET_CHARACTERS_SUCCESS'
Add the action type(s) definition via an interface
export interface GetCharactersBeginAction { type: typeof GET_CHARACTERS_BEGIN, payload: undefined } export interface GetCharactersSuccessAction { type: typeof GET_CHARACTERS_SUCCESS, payload: ICharacterResponse }
Update the action types that can be passed to the reducer
export type ActionTypes = GetCharactersBeginAction | GetCharactersSuccessAction
Update the reducer to handle that action type
export const characterStateReducer = (state: ICharactersState = charactersInitialState, action: ActionTypes, ) => { switch (action.type) { case GET_CHARACTERS_BEGIN: return { ...state, isLoading: true, } case GET_CHARACTERS_SUCCESS: return { ...state, isLoading: false, characters: action.payload.results } default: return { ...state } } }
As a side note, we use the spread operator to update the state and return a new object keeping the state immutable
In the file that you want to update the store from, add a reference to dispatch. Then create the dispatch variable to use, and finally call dispatch with the action creater that you want to call
import { connect, useDispatch } from "react-redux" const dispatch = useDispatch(); dispatch(getCharacters())
That should be all you need to know. If you have questions or want me to clarify anything let me know.