useReducer
looks a lot like Redux, and works well with useContext
connect
and enables new patternsloadSomethingForMe()
and get {isLoading, data, error}
automaticallyuseReducer
, Apollo+GraphQL, Suspense, etc, are useful tools that can overlap with some of the ways Redux is used,
but there's still many valid reasons to choose and use Redux / React-Redux:connect
using React hooks API + internal Redux store subscriptionsuseRedux()
-type hooks APIuseSelector
and useDispatch
connect
, cannot enforce top-down nested subscriptions===
) equality checks for selectors instead of shallow equalityconnect
is supported indefinitely, but no plans for further changesconnect
does:useSelector()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import React from 'react' import { useSelector } from 'react-redux' export const PostsList = () => { const posts = useSelector(state => state.posts) const renderedPosts = posts.map(post => ( <article className="post-excerpt" key={post.id}> <h3>{post.title}</h3> <p>{post.content.substring(0, 100)}</p> </article> )) return ( <section> <h2>Posts</h2> {renderedPosts} </section> ) }
connect
and mapState
:
useDispatch()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import React, { useState } from 'react' import { useDispatch } from 'react-redux' import { postAdded } from './postsSlice' export const AddPostForm = () => { const [title, setTitle] = useState('') const [content, setContent] = useState('') const dispatch = useDispatch() const onSavePostClicked = () => { dispatch( postAdded({title, content }) ) } return ( <section> <h2>Add a New Post</h2> <form> {/* omit form inputs */} <button type="button" onClick={onSavePostClicked}> Save Post </button> </form> </section> ) }
dispatch
methodconnect
and mapDispatch
:
connect
with no mapDispatch
argumentdispatch()
in your own handlersuseMutableSource
hook specifically for use with external state tools like ReduxuseMutableSource
whenever CM finally is readyconnect
will be fully CM-compat (might just take extra work)connect
do internally?create-react-app
and apollo-boost
.@reduxjs/toolkit
) for 1.0.4 release@phryneas
)configureStore()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { configureStore } from "@reduxjs/toolkit"; import todosReducer from "./todos/todosReducer"; import visibilityReducer from "./visibility/visibilityReducer"; const store = configureStore({ reducer: { todos: todosReducer, visibility: visibilityReducer } }); /* The store has been created with these options: - The slice reducers automatically passed to combineReducers() - Added redux-thunk and mutation detection middleware - DevTools Extension is enabled (w/ "action stack traces") - Middleware and devtools enhancers were composed */
createStore
function:
redux-thunk
by default, plus middleware to check for accidental mutations and non-serializable valuescombineReducers
for youconfigureStore()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import {configureStore, combineReducers} from "@reduxjs/toolkit"; // Examples of adding a middleware and a store enhancer import logger from "redux-logger"; import { reduxBatch } from "@manaflair/redux-batch"; import todosReducer from "./todos/todosReducer"; import visibilityReducer from "./visibility/visibilityReducer"; const rootReducer = combineReducers({ todos: todosReducer, visibility: visibilityReducer }); const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger), devTools: NODE_ENV !== "production", preloadedState: {}, enhancers: [reduxBatch] });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import produce from "immer"; // Plain JS with object spread and map return { ...state, first: { ...state.first, second: state.first.second.map((item, i) => { if (i !== index) return item; return { ...item, value: 123 }; }) } }; // Immer return produce(state, draft => { // "Mutating" the draft here is safe - it's a Proxy wrapper! draft.first.second[index].value = 123; });
createReducer()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { createReducer } from "@reduxjs/toolkit"; function todoAdded(state, action) { // Can safely call state.push() here state.push({ text: action.payload, completed: false }); } function todoToggled(state, action) { const { index } = action.payload; const todo = state[index]; // Can directly modify the todo object todo.completed = !todo.completed; } const todosReducer = createReducer([], { "todos/todoAdded": todoAdded, "todos/todoToggled" : todoToggled });
createReducer
with Immer inside. Otherwise, these functions are
mutating the state!createAction()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import { createAction, createReducer } from "@reduxjs/toolkit"; const todoAdded = createAction("todos/todoAdded"); console.log(todoAdded("Buy milk")); // {type : "todos/todoAdded", payload : "Buy milk"} console.log(todoAdded.toString()); // "todos/todoAdded" console.log(todoAdded.type); // "todos/todoAdded" const todosReducer = createReducer([], builder => { // "Builder" syntax lets you pass action creators builder.addCase(todoAdded, (state, action) => { state.push({ text: action.payload, completed: false }); }); // Alternately, can use the older "object" syntax });
redux-actions
payload
as an argumenttoString()
, so it can be used as the "action type" itself where needed (also exposed as actionCreator.type
)createSlice()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
import { createSlice } from "@reduxjs/toolkit"; const userSlice = createSlice({ name: "user", initialState: { name: "", age: 20 }, reducers: { // mutate the state all you want with immer userUpdated(state, { payload }) { state[payload.field] = payload.value; } } }); export const { userUpdated } = userSlice.actions; export default userSlice.reducer; // "Ducks" - quack! // Use this elsewhere in the app: import userReducer, { userUpdated } from "./userSlice"; const rootReducer = combineReducers({ user: userReducer }); store.dispatch(userUpdated({ field: "name", value: "Eric" }));
autodux
projectcreateReducer
utility, so that your reducers can "mutate" their statecreateSlice()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
import { createSlice } from "@reduxjs/toolkit"; import { incremented} from "./counterSlice"; const userSlice = createSlice({ name: "user", initialState: { name: "Fred", age: 20, pets: [] }, reducers: { userUpdated(state, { payload }) { state[payload.field] = payload.value; } }, extraReducers(builder) { // Handle other action types here builder.addCase(incremented, (state, action) => { state.age++; }); } }); export const { userUpdated } = userSlice.actions; export default userSlice.reducer; // "Ducks" - quack! // Write side effects logic alongside, like thunks: export const getUserPets = name => async dispatch => { const userPets = await fetchPets(name); dispatch(userUpdated({ pets: userPets })); dispatch(incremented()); };
extraReducers
createSelector()
1 2 3 4 5 6 7 8 9 10 11
import { createSelector } from "@reduxjs/toolkit"; const selectA = state => state.a; const selectB = state => state.b; const selectABC = createSelector( [selectA, selectB], (a, b, someField) => { return a + b; } );
createSelector
function from Reselect, for creating memoized selectorscreateAsyncThunk()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { userAPI } from "./userAPI"; const fetchUserById = createAsyncThunk( "users/fetchByIdStatus", async (userId, thunkAPI) => { const response = await userAPI.fetchById(userId); return response.data; } ); const usersSlice = createSlice({ name: "users", initialState: { entities: [], loading: "idle" }, reducers: { // standard reducer logic, with auto-generated actions }, extraReducers(builder) { // Add reducers for additional action types here builder.addCase(fetchUserById.fulfilled, (state, action) => { state.entities.push(action.payload); }); } }); // Later, dispatch the thunk as needed in the app dispatch(fetchUserById(123));
createAsyncThunk
implements that pattern:
pending
, fulfilled
, and rejected
casescreateEntityAdapter()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; const booksAdapter = createEntityAdapter({ // Assume IDs are stored in a field other than `book.id` selectId: book => book.bookId, // Keep the "all IDs" array sorted based on book titles sortComparer: (a, b) => a.title.localeCompare(b.title) }); const booksSlice = createSlice({ name: "books", initialState: booksAdapter.getInitialState(), reducers: { // Can pass adapter functions directly as case reducers bookAdded: booksAdapter.addOne, booksReceived(state, action) { // Or, call them as "mutating" helpers in a case reducer booksAdapter.setAll(state, action.payload.books); } } }); // Can create a set of memoized selectors const booksSelectors = booksAdapter.getSelectors(state => state.books); const allBooks = booksSelectors.selectAll(store.getState());
createEntityAdapter
implements that pattern:
mapState
, etc)mapState
, etc)redux-immutable-state-invariant
configureStore()
adds a port of redux-immutable-state-invariant
by default!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// features/pokemon/pokemonService.ts import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; // Define a service using a base URL and expected endpoints export const pokemonApi = createApi({ reducerPath: "pokemonApi", baseQuery: fetchBaseQuery({ baseUrl: "https://pokeapi.co/api/v2/" }), endpoints: (builder) => ({ getPokemonByName: builder.query({ query: (name: string) => `pokemon/${name}`, }), }), }); // Export hooks for usage in functional components, which are // auto-generated based on the defined endpoints export const { useGetPokemonByNameQuery } = pokemonApi; // in a component: function PokemonEntry() { // Using a query hook automatically fetches data and returns query values const { data, error, isLoading } = useGetPokemonByNameQuery("bulbasaur"); }
npm i @reduxjs/toolkit@next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
export const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: "/" }), endpoints: (build) => ({ getMessages: build.query({ query: (channel) => `messages/${channel}`, async onCacheEntryAdded( arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved } ) { // wait for the initial query to resolve before proceeding await cacheDataLoaded; // Update our query result when messages are received const unsubscribe = ChatAPI.subscribeToChannel( arg.channelId, (message) => { // Dispatches an update action with the diff updateCachedData((draft) => { draft.push(message); }); } ); // Clean up when cache subscription is removed await cacheEntryRemoved; unsubscribe(); }, }), }), });
createEntityAdapter
to normalizeuseMyListQuery()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// app/store.ts const store = configureStore({ reducer: { posts: postsReducer, users: usersReducer } }) // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType<typeof store.getState> // Inferred type: // {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch // app/hooks.ts import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import type { RootState, AppDispatch } from './store' // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch<AppDispatch>() export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
RootState
and AppDispatch
types from the store itselfRootState
in inline selectorsdispatch
not recognizing thunks as dispatchable values1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// features/counter/counterSlice.ts import { createSlice, PayloadAction } from "@reduxjs/toolkit"; // Define a type for the slice state interface CounterState { value: number; } // Define the initial state using that type const initialState: CounterState = { value: 0 }; export const counterSlice = createSlice({ name: "counter", initialState, // `createSlice` will infer the state type reducers: { // Use the PayloadAction type to declare `action.payload` amountAdded(state, action: PayloadAction<number>) { state.value += action.payload; }, }, }); // features/counter/Counter.ts import { useAppSelector, useAppDispatch } from "app/hooks"; export function Counter() { // The `state` arg is correctly typed as `RootState` already const count = useAppSelector((state) => state.counter.value); const dispatch = useAppDispatch(); }
PayloadAction<T>
to declare action contentscreate-react-app --template redux
(or --template redux-typescript
)/reducers
folder, "container components", const ADD_TODO = 'ADD_TODO'
)"todos/todoAdded"
types"domain/eventName"
connect
as default - rewrite in progressuseMutableSource