Redux is a state management system for cross-component or app-wide state.
There is three kind of states.
Local state : a state belongs to a single component (E.g listening to user input, toggling a details)
-> should be managed by component-internal with useState or useReducer
Cross-Component State : State that affects multiple components (E.g When we open modal , The open trigger exists outside of modal(Maybe app component), But when we close modal, its trigger exists in modal(The component which it is))
-> requires 'prop chains' / 'prop drilling'
App-wide State : State that affects the entire app(E.g user authentication status)
-> requires 'prop chains' / 'prop drilling'
We treated 2 and 3 case with React Context API. Then Why we need Redux?
Context have potential disadvantages.
1. In huge size app, if we use context, we will end up with deeply nested context provider in JSX return or with too huge one provider which includes complex logics.
Of course, Redux does not suffer from these advantages.
Redux has one central data(state) store in your app.
The process is :
Central Data store -> subscription -> components
What is important in here is There is opposite direction of this process (one direction).
It means that components never can manipulate data in data store.
Instead, it will have circulation process.
Reducer Function(changes stored data) -> Central Store(send Data) -> components -> Action(dispatched from component) -> Reducer Function (triggered)
const redux = require("redux");
//default parameter value for prevent 'undefined' state
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
return state;
}; //takes the given input and produces expected output
const store = redux.createStore(counterReducer); //initialization
const counterSubscriber = () => {
const currentState = store.getState(); //latest state snapshot
console.log(currentState);
};
store.subscribe(counterSubscriber); //tracking state changes
store.dispatch({ type: "increment" }); //dispatch action
store.dispatch({ type: "decrement" });
Set reducer function - The function should be pure function which takes give input and produces expected output always. Additionally, you mignt have to give default parameter if you want to avoid 'undefined' error.
create Store - After you create store, This line will initialize your reducer function when the code run at the first time.
create subscriber - This subscriber function have state from store and You can get latest snapshot of state with 'getState()'
To track state changes, use method '.subscribe()' and the argument is your subscriber
Dispatch action to invoke reducer function. You can set type of action in dispatch as an object
First of all, For easier approach to use Redux in react, Install both 'redux' and 'react-redux' package with npm.
Create store file in separated directory from 'src'
I created file in the path 'store/index.js'
Build store structure.
We don't need subscriber in this store file because we wanna use it in other components.
import {createStore} from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
return state;
};
const store = createStore(counterReducer);
export default store;
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
import store from "./store/index";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
import { useSelector, connect } from "react-redux";
import classes from "./Counter.module.css";
const Counter = () => {
const counter = useSelector((state) => state.counter);
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch({ type: "increment" });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
.
.
class Counter extends Component {
incrementHandler() {
this.props.increment();
}
decrementHandler() {
this.props.decrement();
}
toggleCounterHandler() {}
render() {
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{this.props.counter}</div>
<div>
<button onClick={this.incrementHandler.bind(this)}>Increment</button>
<button onClick={this.decrementHandler.bind(this)}>Decrement</button>
</div>
<button onClick={this.toggleCounterHandler}>Toggle Counter</button>
</main>
);
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter,
};
};
const mapDisptachToProps = (dispatch) => {
return {
increment: () => dispatch({ type: "increment" }),
decrement: () => dispatch({ type: "decrement" }),
};
};
export default connect(mapStateToProps, mapDisptachToProps)(Counter);
const increaseHandler = () => {
dispatch({ type: "increase", value: 5 });
};
const initialState = { counter: 0, showCounter: true };
const counterReducer = (state = initialState, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
showCounter: state.showCounter,
};
}
Because redux doesn't merge prev state with updated state, just replace old one, you should define all property if you want to keep state as it was.
Here are two important factors.
One is you should not change exisiting state, you should override existing state by returning a new state object. It is just because javascript object is reference value.
The other is , As i already said, you should keep returning same state if you want to avoid unexpected outcome.
Here are some potential problems we have.
During our process, we will get many action type identifier and many state in reducer function. If there is a little typo in identifier, it is really hard to figure out what is going wrong. Also if many state exists in reducer function, it makes us difficult to understand our logic. Another problem is state immutability which we already mentioned.
To solve this problems use 'Redux Toolkit'
createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {state.counter++;},
derement(state) {state.counter--},
increase(state, action) {state.counter += action.value},
toggle(state) {state.showCounter = !state.showCounter},
},
});
we can mutate state here because this toolkit have internal method which recognized that both old and current state share same reference.
4. updated code will be like below
import { createSlice, configureStore } from "@reduxjs/toolkit";
const initialState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.counter++;
},
derement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.value;
},
toggle(state) {
state.showCounter = !state.showCounter;
},
},
});
const store = configureStore({
reducer: counterSlice.reducer,
});
export default store;
export const counterActions = counterSlice.actions;
const incrementHandler = () => {
dispatch(counterActions.increment());
};
The reason that we execute this action function is it generates its own identifier by invoking.
You also can include payload.
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
auth: authSlice.reducer,
},
});
And also it is slightly different of using actions in component
When we just use one state -> const isAuthenticated = useSelector((state) => state.isAuthenticated);
When we use multiple states -> const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
(you should approach reducer property first.)
import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./counter";
import authSlice from "./auth";
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
auth: authSlice.reducer,
},
});
export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;
export default store;
The process is not different.