Once upon a time, class components were the only way to hook into lifecycle methods. This, for example, allowed the component to load initial data via Ajax on mount. The lifecycle method componentDidMount
worked well for this use case. This forced the component to manage its own state and did not come without faults.
One gotcha was being able to manage the state with Redux. When Redux wanted to reload component data, it was hard to do without ugly hacks. This is because it was doing state management in two places, both in the component itself and Redux. As class component complexity grew, so did the hacks.
Enter Hooks, a new way to manage state without class components during initial load. The idea is to let the Redux store manage state without lifecycle methods. This allows code reuse because state management gets encapsulated in the store. When dispatch actions fire across, stored state
has all there is to know about each component.
In this take, we’ll delve into Hooks to tackle this common use case. Loading initial component data while letting Redux manage state has its benefits. To keep focus on relevant code, we’ll skip setting up a new code project. All code samples are on GitHub.
Redux State to Props
At initial load, there are three props we care about: isInit
, isLoading
, and isError
. The isInit
prop signals to the component that it is time to begin loading data. While Ajax is waiting for a response, isLoading
can show a user-friendly loader in the UI. Then, if there are any errors, isError
puts the component in an error state.
Therefore, the initial state in Redux can be:
const initialState = {
firstName: '',
isInit: false,
isLoading: false,
isError: false
};
We appended firstName
to the state since it is the data coming back as the response. Because Redux encourages a single store, this response data can live anywhere. For example, a parent component can manage loading state, while a child component encapsulates response data. To keep this example simple, we put this in the same hierarchy.
Redux-Thunk
These three props act like control flags. The goal is to support every event that happens during load. To flip each flag, create these action types:
const DUMMY_LOADING_STATE_DATA = 'DUMMY_LOADING_STATE_DATA';
const DUMMY_UPDATE_STATE_DATA = 'DUMMY_UPDATE_STATE_DATA';
const DUMMY_ERROR_STATE_DATA = 'DUMMY_ERROR_STATE_DATA';
Fire dispatch messages across the store by creating action functions with payload
or error
:
export const showLoadingState = () => ({type: DUMMY_LOADING_STATE_DATA});
export const updateStateData = (state) => ({type: DUMMY_UPDATE_STATE_DATA, payload: state });
export const errorStateData = (reason) => ({type: DUMMY_ERROR_STATE_DATA, payload: reason, error: true })
Then, put in place the Redux-Thunk that begins initial load:
const loadInitStateData = () => async (dispatch, getState, axios) => {
dispatch(showLoadingState());
try {
const url = '/person.json';
const response = await axios.get(url);
return dispatch(updateStateData(response.data));
} catch (reason) {
return dispatch(errorStateData(reason.message));
}
};
Note the dispatch showLoadingState
; this signals the component to wait for response data. On success, payload
has the Ajax response. On failure, reason
has the error message with an error
flag. This is a useful pattern to send dispatches in Redux.
Dispatch actions that fire across the store have the following properties:
- type: Required. Message event taking place during load.
- payload: Optional. Object with response data on success, or error message on failure.
- error: Optional. A boolean that when true says the payload contains an error message.
Finally, all the reducer must do is manage the loading state:
const dummyReducer = (state = initialState, action) => {
switch (action.type) {
case DUMMY_LOADING_STATE_DATA:
return {...state, isLoading: true, isInit: true};
case DUMMY_UPDATE_STATE_DATA:
const {firstName} = action.payload;
return {...state, firstName: firstName, isLoading: false};
case DUMMY_ERROR_STATE_DATA:
return {...state, isError: true, isLoading: false};
default:
return state;
}
};
The initialState
is set so the component can begin loading. Redux persists isInit
state once this control flag flips to true, so it knows not to reload. This is because Redux works like a state machine. The spread ...state
maintains previous state data in the store. One gotcha is to be sure to set isLoading
to false on error. This is to prevent the component from appearing frozen in the UI. Customers might get the wrong idea when components never load and do not land in a proper error state.
Functional Component
With all the hard work done in the Redux store, the component can focus on a single concern:
const DummyComponent = ({isInit, isError, isLoading, loadInitStateData, firstName}) =>
<>
{useEffect(() => {!isInit && loadInitStateData()})}
{isLoading && (<p>Loading...</p>)}
{isError && (<p>An error occurred.</p>)}
{!isLoading && !isError && isInit && (<p>Hello {firstName}.</p>)}
</>;
Each control flag flips certain pieces of the UI. The props drive behavior depending on how each is set in the object parameter. This makes the component testable because it follows a functional paradigm. Each permutation of inputs has a one-to-one relationship to the rendered output. Note the use of useEffect
. This is a Hook that executes at re-render but notice the dispatch is behind a control flag. This gives control back over to Redux so it can do its job, which is to manage the state.
Be sure not to wrap Hooks around any conditional logic. React catches this during render and fails to mount the component. A better idea is to put any control flags inside the callback. This is because functional components must execute Hooks at re-render.
Conclusion
Hooks are an exciting new way to work with functional components.
This is one way React components embrace the functional paradigm. With a Redux store, Hooks level up because it benefits from this state machine. By letting Redux manage state, it keeps separation of concerns and clean code.
As a closing note, pay special attention to JavaScript source code protection if you're developing commercial or enterprise apps. You can do this by starting a free Jscrambler trial - and don't miss our guide for protecting React apps.
Top comments (0)