npx create-react-app redux-essentials-example --template redux
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
counterReducer : reducerexport const store = configureStore({
reducer: {
counter: counterReducer,
},
});
configureStore를 호출하여 Redux store 생성reducer 객체는 앱의 상태를 관리하는 reducer 정의counter라는 상태 slice를 counterReducer가 관리하도록 설정import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
state.users, state.posts, and state.comments : seperate slice of the Redux stateimport { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payloaㅔd
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
import { createSlice } from '@reduxjs/toolkit'
createSlice : Reducer와 Action Creator 함께 정의할 수 있도록 허용함action type strings, action creator functions and action objects.name option is used as the first part of each action type"counter" name + the "increment" reducer function generated an action type of { type: "counter/increment" }console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
Our reducers are never allowed to mutate the original / current state values !
state.value = 123 // Illegal
return {
...state,
value: 123
}
// 기존 방식
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
// createSlice 이용 방식
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
You can only write "mutating" logic in Redux Toolkit's createSlice and createReducer because they use Immer inside! If you write mutating logic in reducers without Immer, it will mutate the state and cause bugs!
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
dispatch and getState as arguments// The function below is called a thunk and allows us to perform async logic.
// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
// This will call the thunk with the `dispatch` function as the first argument.
// Async code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
// We can use them the same way we use a typical Redux action creator
store.dispatch(incrementAsync(5))
// the outside "thunk creator" function
const fetchUserById = userId => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
// make an async call in the thunk
const user = await userAPI.fetchById(userId)
// dispatch an action when we get the response back
dispatch(userLoaded(user))
} catch (err) {
// If something went wrong, handle it here
}
}
}
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
const count = useSelector(selectCount)
React includes several built-in hooks like useState and `useEffect
Other libraries can create their own custom hooks that use React's hooks to build custom logic
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value
const count = selectCount(store.getState())
console.log(count)
// 0
useSelector takes care of talking to the Redux store behinds the scenes for us.someSelector(store.getState()) for us and returns the resultconst count = useSelector(selectCount)
const countPlusTwo = useSelector(state => state.counter.value + 2)
const dispatch = useDispatch()
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
Do I always have to put all my app's state into the Redux store? NO.
const [incrementAmount, setIncrementAmount] = useState('2')
// later
return (
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
)
Keep the current number string in the Redux store by dispatching an action in the input's onChange handler and keep it in our reducer
dispatch(incrementAsync(Number(incrementAmount) || 0))}
React+Redux App
- Global State -> Redux store
useSelector and useDispatch hooks to talk to the Redux store// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
ReactDOM.render(<App />) - Tell React to start rendering our root ` Component<Provider>useSelector) to work, use component to pass down the Redux store