Introduction
You've already heard of Redux as the best state management tool for React especially, and no doubt it is best, but in most of the cases or in medium-level apps we might don't need Redux.
What is Redux?
Redux works with three main things, action, reducers & store.
Action dispatches with payload and type, reducer manipulate or store the state in Redux store
I saw many people using redux in their small apps because they think they should use redux for state management, but sometimes the app is not much complex to use a tool like this, so in this case, what should you do?
React 16 and Hooks:
In React 16 many hooks are introduced to make things easy and now we can only work with functional components and using hooks to make things easy for all of us.
There are many hooks in the list
Basic Hooks
- useState
- useEffect
- useContext
Other Hooks
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
The Context API and useReducer:
Some of the basic hooks we used daily in our React App, but we have two main hooks that can re-write or replace the Redux logic and its implementation, and those are useReducer
& useContext
(Context API), let's see them in action.
Create a new react app with this command:
create-react-app myapp
In your src
folder create a new folder called state
and in this create a new file called `posts.js, and insert code below
// posts.js
export const initialState = {
loading: false,
posts: [],
};
export const actions = {
GET_POSTS: 'GET_POSTS',
GET_POSTS_SUCCESS: 'GET_POSTS_SUCCESS',
GET_POSTS_FAILURE: 'GET_POSTS_FAILURE',
};
export const postReducer = (state, action) => {
switch (action.type) {
case actions.GET_POSTS:
return { ...state, loading: true };
case actions.GET_POSTS_SUCCESS:
return {
...state,
message: 'Posts fetched successfully',
posts: action.payload,
loading: false,
};
case actions.GET_POSTS_FAILURE:
return { ...state, message: 'Something went wrong', loading: false };
default:
return state;
}
};
- In the code above first, we have
initialState
of an app, - After that, we have some
action
types which can be any string or enums you want. - And at the end, we have our
postReducer
which will be responsible to mutate the state and update UI accordingly.
All these things are just pure javascript and nothing else.
Now in src/App.js
,
//App.js
import { useEffect, useReducer } from 'react';
import {
actions,
initialState,
postReducer,
} from './state/posts';
import './App.css';
function App() {
const [state, dispatch] = useReducer(postReducer, initialState);
useEffect(() => {
dispatch({
type: actions.GET_POSTS,
});
async function fetchPosts() {
try {
const posts = await (
await fetch('https://jsonplaceholder.typicode.com/posts')
).json();
dispatch({
type: actions.GET_POSTS_SUCCESS,
payload: posts,
});
} catch (error) {
dispatch({
type: actions.GET_POSTS_FAILURE,
});
}
}
fetchPosts();
}, []);
return (
<div>
{state.loading && <p>LOADING ........</p>}
<ul>
{state.posts?.map((post) => {
return (
<div key={post.id}>
<li>
<b>
<h3>{post.title}</h3>
</b>
<p>{post.body}</p>
</li>
<hr />
</div>
);
})}
</ul>
</div>
);
}
export default App;
- In the code above we have use the
useReducer
hook and passed it in ourpostReducer
andinitialState
fromsrc/state/posts.js
. - Then in the
useEffect
hook, we have some data from API and after fetching data we are dispatching an action to save that data into the state of the app. - And now in UI we are simply mapping through the data show it on screen.
So now our reducer is ready to make an action, but wait there is one problem.
Suppose we have <PostList/>
components have we have to get the state of an app in child component that is <PostList/>
, so now if you use useReducer
in another child component you will not get any state but an empty array.
But why?
Because our data is dispatched from Parent component and cannot be avaialble to it's child unlsess we passes it from the props
or saved that data into our Global Context or Store Context.
So here comes the context API.
In src/state/posts.js
create a new variable called StoreContext
like this:
//posts.js
import React from 'react';
export const StoreContext = React.createContext(null);
// further code
in src/App.js
, wrap the component with Context provider and pass the app state and dispatcher to it like this:
//App.js
// above code
const [state, dispatch] = useReducer(postReducer, initialState);
<StoreContext.Provider value={{ dispatch, state }}>
<div>
{state.loading && <p>LOADING ........</p>}
<ul>
{state.posts?.map((post) => {
return (
<div key={post.id}>
<li>
<b>
<h3>{post.title}</h3>
</b>
<p>{post.body}</p>
</li>
<hr />
</div>
);
})}
</ul>
</div>
</StoreContext.Provider>
// further code
Now create a new component in the src
folder called PostList.js
and insert code below:
//PostList.js
import React, { useContext } from 'react';
import { StoreContext } from './state/posts';
export const Posts = () => {
const store = useContext(StoreContext);
const state = store.state;
return (
<div>
<ul>
{state.posts?.map((post) => {
return (
<div key={post.id}>
<li>
<b>
<h3>{post.title}</h3>
</b>
<p>{post.body}</p>
</li>
<hr />
</div>
);
})}
</ul>
</div>
);
};
- Here you can use the
useContext
hook and pass it the StoreContext you created before, and now all the state and its dispatcher will be available to it's a child or even grandchild components.
Conclusion
This doesn't mean you shouldn't use Redux but if we have a small or medium-size app which can share data easily then we should use the power of context API and useReducer, and make our own redux structure that can be easily customizable and scalable.