Let's implement the counter example in all the four ways: Prop Drilling, Context API, Redux, and Redux Toolkit.
1. Prop Drilling
In prop drilling, the state and the functions that modify the state are passed down as props through intermediate components.
// App.js
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return <Parent count={count} setCount={setCount} />;
}
function Parent({ count, setCount }) {
return <Child count={count} setCount={setCount} />;
}
function Child({ count, setCount }) {
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default App;
Explanation:
Prop Drilling: The count state and the setCount function are passed from App to Parent, and finally to Child where the UI is updated. This works fine for smaller apps but can get messy when the state is passed through many layers of components.
2. Context API
Using the Context API, we avoid prop drilling by making the state and functions available globally to the component tree.
// App.js
import React, { useState, createContext, useContext } from "react";
// Create Context
const CounterContext = createContext();
function App() {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{ count, setCount }}>
<Parent />
</CounterContext.Provider>
);
}
function Parent() {
return <Child />;
}
function Child() {
const { count, setCount } = useContext(CounterContext);
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default App;
Explanation:
Context API: We create a CounterContext that holds the state and the setCount function. The Child component consumes the context via useContext. This approach prevents prop drilling by making the state accessible to any component in the tree.
3. Redux
In the Redux approach, we use a global store to manage the state.
1. Install Redux:
npm install redux react-redux
2. Create Redux Store:
// store.js
import { createStore } from "redux";
// Initial state
const initialState = {
count: 0,
};
// Reducer function
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
// Create Redux store
const store = createStore(counterReducer);
export default store;
3. App Component:
// App.js
import React from "react";
import { Provider } from "react-redux";
import Parent from "./Parent";
import store from "./store";
function App() {
return (
<Provider store={store}>
<Parent />
</Provider>
);
}
export default App;
4. Parent and Child Components:
// Parent.js
import React from "react";
import Child from "./Child";
function Parent() {
return <Child />;
}
export default Parent;
// Child.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
function Child() {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
</div>
);
}
export default Child;
Explanation:
Redux: The state is centralized in the Redux store, and the Child component accesses the store using useSelector to get the count, and useDispatch to dispatch the INCREMENT and DECREMENT actions.
4. Redux Toolkit
Redux Toolkit simplifies Redux setup by generating actions and reducers automatically.
1. Install Redux Toolkit:
npm install @reduxjs/toolkit react-redux
2. Create Slice:
// counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
// Create a slice
const counterSlice = createSlice({
name: "counter",
initialState: {
count: 0,
},
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
3. Configure Store:
// store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
// Configure the store
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
4. App Component:
// App.js
import React from "react";
import { Provider } from "react-redux";
import Parent from "./Parent";
import store from "./store";
function App() {
return (
<Provider store={store}>
<Parent />
</Provider>
);
}
export default App;
5. Parent and Child Components:
// Parent.js
import React from "react";
import Child from "./Child";
function Parent() {
return <Child />;
}
export default Parent;
// Child.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./counterSlice";
function Child() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
}
export default Child;
Explanation:
Redux Toolkit: The counterSlice is created using createSlice, which automatically generates the action creators (increment, decrement) and the reducer. The store is set up with configureStore. The Child component accesses the state and dispatches actions in the same way as with traditional Redux, but the setup is more concise.
Summary of All Approaches:
Each method serves a specific use case, and as your app scales in complexity, moving from Prop Drilling to Redux or Redux Toolkit will make managing the state easier.