diff --git a/packages/cra-template-redux-typescript/template/src/features/counter/counterSlice.ts b/packages/cra-template-redux-typescript/template/src/features/counter/counterSlice.ts index 07bc1f5c..702af72e 100644 --- a/packages/cra-template-redux-typescript/template/src/features/counter/counterSlice.ts +++ b/packages/cra-template-redux-typescript/template/src/features/counter/counterSlice.ts @@ -1,89 +1,100 @@ +// This file demonstrates typical usage of Redux Toolkit's createSlice function +// for defining reducer logic and actions, as well as related thunks and selectors. + import type { PayloadAction } from "@reduxjs/toolkit" -import { createAppSlice } from "../../app/createAppSlice" -import type { AppThunk } from "../../app/store" +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" +import type { AppThunk, RootState } from "../../app/store" import { fetchCount } from "./counterAPI" -export interface CounterSliceState { +// Define the TS type for the counter slice's state +export interface CounterState { value: number status: "idle" | "loading" | "failed" } -const initialState: CounterSliceState = { +// Define the initial value for the slice state +const initialState: CounterState = { value: 0, status: "idle", } -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ +// Slices contain Redux reducer logic for updating state, and +// generate actions that can be dispatched to trigger those updates. +export const counterSlice = createSlice({ name: "counter", - // `createSlice` will infer the state type from the `initialState` argument initialState, // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { + 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: create.reducer(state => { + }, + decrement: state => { state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer( - (state, action: PayloadAction) => { + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: builder => { + builder + // Handle the action types defined by the `incrementAsync` thunk defined below. + // This lets the slice reducer update the state with request status and results. + .addCase(incrementAsync.pending, state => { + state.status = "loading" + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = "idle" state.value += action.payload - }, - ), - // 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. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, + }) + .addCase(incrementAsync.rejected, state => { + state.status = "failed" + }) }, }) -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions +// Export the generated action creators for use in components +export const { increment, decrement, incrementByAmount } = counterSlice.actions -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors +// Export the slice reducer for use in the store configuration +export default counterSlice.reducer -// We can also write thunks by hand, which may contain both sync and async logic. +// Selector functions allows us to select a value from the Redux root state. +// Selectors can also be defined inline in the `useSelector` call +// in a component, or inside the `createSlice.selectors` field. +export const selectCount = (state: RootState) => state.counter.value +export const selectStatus = (state: RootState) => state.counter.status + +// The function below is called a thunk, which can contain both sync and async logic +// that has access to both `dispatch` and `getState`. They can be dispatched like +// a regular action: `dispatch(incrementIfOdd(10))`. // Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { +export const incrementIfOdd = (amount: number): AppThunk => { + return (dispatch, getState) => { const currentValue = selectCount(getState()) - - if (currentValue % 2 === 1 || currentValue % 2 === -1) { + if (currentValue % 2 === 1) { dispatch(incrementByAmount(amount)) } } +} + +// Thunks are commonly used for async logic like fetching data. +// The `createAsyncThunk` method is used to generate thunks that +// dispatch pending/fulfilled/rejected actions based on a promise. +// In this example, we make a mock async request and return the result. +// The `createSlice.extraReducers` field can handle these actions +// and update the state with the results. +export const incrementAsync = createAsyncThunk( + "counter/fetchCount", + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, +) diff --git a/packages/cra-template-redux-typescript/template/src/features/counter/counterSliceAdvanced.ts b/packages/cra-template-redux-typescript/template/src/features/counter/counterSliceAdvanced.ts new file mode 100644 index 00000000..420944d9 --- /dev/null +++ b/packages/cra-template-redux-typescript/template/src/features/counter/counterSliceAdvanced.ts @@ -0,0 +1,93 @@ +// This file has the exact same functionality as `counterSlice.ts`, +// but it uses the newest features from Redux Toolkit 2.0. +// These are optional, but may simplify some of your code. + +import type { PayloadAction } from "@reduxjs/toolkit" +import { createAppSlice } from "../../app/createAppSlice" +import type { AppThunk } from "../../app/store" +import { fetchCount } from "./counterAPI" + +export interface CounterSliceState { + value: number + status: "idle" | "loading" | "failed" +} + +const initialState: CounterSliceState = { + value: 0, + status: "idle", +} + +// If you are not using async thunks you can use the standalone `createSlice`. +export const counterSlice = createAppSlice({ + name: "counter", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: create => ({ + increment: create.reducer(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: create.reducer(state => { + state.value -= 1 + }), + // Use the `PayloadAction` type to declare the contents of `action.payload` + incrementByAmount: create.reducer( + (state, action: PayloadAction) => { + state.value += action.payload + }, + ), + // 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. Thunks are + // typically used to make async requests. + incrementAsync: create.asyncThunk( + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, + { + pending: state => { + state.status = "loading" + }, + fulfilled: (state, action) => { + state.status = "idle" + state.value += action.payload + }, + rejected: state => { + state.status = "failed" + }, + }, + ), + }), + // You can define your selectors here. These selectors receive the slice + // state as their first argument. + selectors: { + selectCount: counter => counter.value, + selectStatus: counter => counter.status, + }, +}) + +// Action creators are generated for each case reducer function. +export const { decrement, increment, incrementByAmount, incrementAsync } = + counterSlice.actions + +// Selectors returned by `slice.selectors` take the root state as their first argument. +export const { selectCount, selectStatus } = counterSlice.selectors + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = + (amount: number): AppThunk => + (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1 || currentValue % 2 === -1) { + dispatch(incrementByAmount(amount)) + } + } diff --git a/packages/cra-template-redux/template/src/features/counter/counterSlice.js b/packages/cra-template-redux/template/src/features/counter/counterSlice.js index 9c17ab8c..048980cd 100644 --- a/packages/cra-template-redux/template/src/features/counter/counterSlice.js +++ b/packages/cra-template-redux/template/src/features/counter/counterSlice.js @@ -1,78 +1,92 @@ -import { createAppSlice } from "../../app/createAppSlice" +// This file demonstrates typical usage of Redux Toolkit's createSlice function +// for defining reducer logic and actions, as well as related thunks and selectors. + +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" import { fetchCount } from "./counterAPI" +// Define the initial value for the slice state const initialState = { value: 0, status: "idle", } -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ +// Slices contain Redux reducer logic for updating state, and +// generate actions that can be dispatched to trigger those updates. +export const counterSlice = createSlice({ name: "counter", - // `createSlice` will infer the state type from the `initialState` argument initialState, // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { + 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: create.reducer(state => { + }, + decrement: state => { state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer((state, action) => { + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action) => { state.value += action.payload - }), - // 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. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async amount => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: builder => { + builder + // Handle the action types defined by the `incrementAsync` thunk defined below. + // This lets the slice reducer update the state with request status and results. + .addCase(incrementAsync.pending, state => { + state.status = "loading" + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = "idle" + state.value += action.payload + }) + .addCase(incrementAsync.rejected, state => { + state.status = "failed" + }) }, }) -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions +// Export the generated action creators for use in components +export const { increment, decrement, incrementByAmount } = counterSlice.actions -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors +// Export the slice reducer for use in the store configuration +export default counterSlice.reducer -// We can also write thunks by hand, which may contain both sync and async logic. -// Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = amount => (dispatch, getState) => { - const currentValue = selectCount(getState()) +// Selector functions allows us to select a value from the Redux root state. +// Selectors can also be defined inline in the `useSelector` call +// in a component, or inside the `createSlice.selectors` field. +export const selectCount = (state) => state.counter.value +export const selectStatus = (state) => state.counter.status - if (currentValue % 2 === 1 || currentValue % 2 === -1) { - dispatch(incrementByAmount(amount)) +// The function below is called a thunk, which can contain both sync and async logic +// that has access to both `dispatch` and `getState`. They can be dispatched like +// a regular action: `dispatch(incrementIfOdd(10))`. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = (amount) => { + return (dispatch, getState) => { + const currentValue = selectCount(getState()) + if (currentValue % 2 === 1) { + dispatch(incrementByAmount(amount)) + } } } + +// Thunks are commonly used for async logic like fetching data. +// The `createAsyncThunk` method is used to generate thunks that +// dispatch pending/fulfilled/rejected actions based on a promise. +// In this example, we make a mock async request and return the result. +// The `createSlice.extraReducers` field can handle these actions +// and update the state with the results. +export const incrementAsync = createAsyncThunk( + "counter/fetchCount", + async (amount) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, +) diff --git a/packages/cra-template-redux/template/src/features/counter/counterSliceAdvanced.js b/packages/cra-template-redux/template/src/features/counter/counterSliceAdvanced.js new file mode 100644 index 00000000..9b432b60 --- /dev/null +++ b/packages/cra-template-redux/template/src/features/counter/counterSliceAdvanced.js @@ -0,0 +1,82 @@ +// This file has the exact same functionality as `counterSlice.ts`, +// but it uses the newest features from Redux Toolkit 2.0. +// These are optional, but may simplify some of your code. + +import { createAppSlice } from "../../app/createAppSlice" +import { fetchCount } from "./counterAPI" + +const initialState = { + value: 0, + status: "idle", +} + +// If you are not using async thunks you can use the standalone `createSlice`. +export const counterSlice = createAppSlice({ + name: "counter", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: create => ({ + increment: create.reducer(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: create.reducer(state => { + state.value -= 1 + }), + // Use the `PayloadAction` type to declare the contents of `action.payload` + incrementByAmount: create.reducer((state, action) => { + state.value += action.payload + }), + // 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. Thunks are + // typically used to make async requests. + incrementAsync: create.asyncThunk( + async amount => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, + { + pending: state => { + state.status = "loading" + }, + fulfilled: (state, action) => { + state.status = "idle" + state.value += action.payload + }, + rejected: state => { + state.status = "failed" + }, + }, + ), + }), + // You can define your selectors here. These selectors receive the slice + // state as their first argument. + selectors: { + selectCount: counter => counter.value, + selectStatus: counter => counter.status, + }, +}) + +// Action creators are generated for each case reducer function. +export const { decrement, increment, incrementByAmount, incrementAsync } = + counterSlice.actions + +// Selectors returned by `slice.selectors` take the root state as their first argument. +export const { selectCount, selectStatus } = counterSlice.selectors + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = amount => (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1 || currentValue % 2 === -1) { + dispatch(incrementByAmount(amount)) + } +} diff --git a/packages/expo-template-redux-typescript/src/features/counter/counterSlice.ts b/packages/expo-template-redux-typescript/src/features/counter/counterSlice.ts index 07bc1f5c..702af72e 100644 --- a/packages/expo-template-redux-typescript/src/features/counter/counterSlice.ts +++ b/packages/expo-template-redux-typescript/src/features/counter/counterSlice.ts @@ -1,89 +1,100 @@ +// This file demonstrates typical usage of Redux Toolkit's createSlice function +// for defining reducer logic and actions, as well as related thunks and selectors. + import type { PayloadAction } from "@reduxjs/toolkit" -import { createAppSlice } from "../../app/createAppSlice" -import type { AppThunk } from "../../app/store" +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" +import type { AppThunk, RootState } from "../../app/store" import { fetchCount } from "./counterAPI" -export interface CounterSliceState { +// Define the TS type for the counter slice's state +export interface CounterState { value: number status: "idle" | "loading" | "failed" } -const initialState: CounterSliceState = { +// Define the initial value for the slice state +const initialState: CounterState = { value: 0, status: "idle", } -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ +// Slices contain Redux reducer logic for updating state, and +// generate actions that can be dispatched to trigger those updates. +export const counterSlice = createSlice({ name: "counter", - // `createSlice` will infer the state type from the `initialState` argument initialState, // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { + 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: create.reducer(state => { + }, + decrement: state => { state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer( - (state, action: PayloadAction) => { + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: builder => { + builder + // Handle the action types defined by the `incrementAsync` thunk defined below. + // This lets the slice reducer update the state with request status and results. + .addCase(incrementAsync.pending, state => { + state.status = "loading" + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = "idle" state.value += action.payload - }, - ), - // 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. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, + }) + .addCase(incrementAsync.rejected, state => { + state.status = "failed" + }) }, }) -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions +// Export the generated action creators for use in components +export const { increment, decrement, incrementByAmount } = counterSlice.actions -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors +// Export the slice reducer for use in the store configuration +export default counterSlice.reducer -// We can also write thunks by hand, which may contain both sync and async logic. +// Selector functions allows us to select a value from the Redux root state. +// Selectors can also be defined inline in the `useSelector` call +// in a component, or inside the `createSlice.selectors` field. +export const selectCount = (state: RootState) => state.counter.value +export const selectStatus = (state: RootState) => state.counter.status + +// The function below is called a thunk, which can contain both sync and async logic +// that has access to both `dispatch` and `getState`. They can be dispatched like +// a regular action: `dispatch(incrementIfOdd(10))`. // Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { +export const incrementIfOdd = (amount: number): AppThunk => { + return (dispatch, getState) => { const currentValue = selectCount(getState()) - - if (currentValue % 2 === 1 || currentValue % 2 === -1) { + if (currentValue % 2 === 1) { dispatch(incrementByAmount(amount)) } } +} + +// Thunks are commonly used for async logic like fetching data. +// The `createAsyncThunk` method is used to generate thunks that +// dispatch pending/fulfilled/rejected actions based on a promise. +// In this example, we make a mock async request and return the result. +// The `createSlice.extraReducers` field can handle these actions +// and update the state with the results. +export const incrementAsync = createAsyncThunk( + "counter/fetchCount", + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, +) diff --git a/packages/expo-template-redux-typescript/src/features/counter/counterSliceAdvanced.ts b/packages/expo-template-redux-typescript/src/features/counter/counterSliceAdvanced.ts new file mode 100644 index 00000000..420944d9 --- /dev/null +++ b/packages/expo-template-redux-typescript/src/features/counter/counterSliceAdvanced.ts @@ -0,0 +1,93 @@ +// This file has the exact same functionality as `counterSlice.ts`, +// but it uses the newest features from Redux Toolkit 2.0. +// These are optional, but may simplify some of your code. + +import type { PayloadAction } from "@reduxjs/toolkit" +import { createAppSlice } from "../../app/createAppSlice" +import type { AppThunk } from "../../app/store" +import { fetchCount } from "./counterAPI" + +export interface CounterSliceState { + value: number + status: "idle" | "loading" | "failed" +} + +const initialState: CounterSliceState = { + value: 0, + status: "idle", +} + +// If you are not using async thunks you can use the standalone `createSlice`. +export const counterSlice = createAppSlice({ + name: "counter", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: create => ({ + increment: create.reducer(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: create.reducer(state => { + state.value -= 1 + }), + // Use the `PayloadAction` type to declare the contents of `action.payload` + incrementByAmount: create.reducer( + (state, action: PayloadAction) => { + state.value += action.payload + }, + ), + // 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. Thunks are + // typically used to make async requests. + incrementAsync: create.asyncThunk( + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, + { + pending: state => { + state.status = "loading" + }, + fulfilled: (state, action) => { + state.status = "idle" + state.value += action.payload + }, + rejected: state => { + state.status = "failed" + }, + }, + ), + }), + // You can define your selectors here. These selectors receive the slice + // state as their first argument. + selectors: { + selectCount: counter => counter.value, + selectStatus: counter => counter.status, + }, +}) + +// Action creators are generated for each case reducer function. +export const { decrement, increment, incrementByAmount, incrementAsync } = + counterSlice.actions + +// Selectors returned by `slice.selectors` take the root state as their first argument. +export const { selectCount, selectStatus } = counterSlice.selectors + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = + (amount: number): AppThunk => + (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1 || currentValue % 2 === -1) { + dispatch(incrementByAmount(amount)) + } + } diff --git a/packages/react-native-template-redux-typescript/template/src/features/counter/counterSlice.ts b/packages/react-native-template-redux-typescript/template/src/features/counter/counterSlice.ts index 07bc1f5c..702af72e 100644 --- a/packages/react-native-template-redux-typescript/template/src/features/counter/counterSlice.ts +++ b/packages/react-native-template-redux-typescript/template/src/features/counter/counterSlice.ts @@ -1,89 +1,100 @@ +// This file demonstrates typical usage of Redux Toolkit's createSlice function +// for defining reducer logic and actions, as well as related thunks and selectors. + import type { PayloadAction } from "@reduxjs/toolkit" -import { createAppSlice } from "../../app/createAppSlice" -import type { AppThunk } from "../../app/store" +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" +import type { AppThunk, RootState } from "../../app/store" import { fetchCount } from "./counterAPI" -export interface CounterSliceState { +// Define the TS type for the counter slice's state +export interface CounterState { value: number status: "idle" | "loading" | "failed" } -const initialState: CounterSliceState = { +// Define the initial value for the slice state +const initialState: CounterState = { value: 0, status: "idle", } -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ +// Slices contain Redux reducer logic for updating state, and +// generate actions that can be dispatched to trigger those updates. +export const counterSlice = createSlice({ name: "counter", - // `createSlice` will infer the state type from the `initialState` argument initialState, // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { + 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: create.reducer(state => { + }, + decrement: state => { state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer( - (state, action: PayloadAction) => { + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: builder => { + builder + // Handle the action types defined by the `incrementAsync` thunk defined below. + // This lets the slice reducer update the state with request status and results. + .addCase(incrementAsync.pending, state => { + state.status = "loading" + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = "idle" state.value += action.payload - }, - ), - // 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. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, + }) + .addCase(incrementAsync.rejected, state => { + state.status = "failed" + }) }, }) -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions +// Export the generated action creators for use in components +export const { increment, decrement, incrementByAmount } = counterSlice.actions -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors +// Export the slice reducer for use in the store configuration +export default counterSlice.reducer -// We can also write thunks by hand, which may contain both sync and async logic. +// Selector functions allows us to select a value from the Redux root state. +// Selectors can also be defined inline in the `useSelector` call +// in a component, or inside the `createSlice.selectors` field. +export const selectCount = (state: RootState) => state.counter.value +export const selectStatus = (state: RootState) => state.counter.status + +// The function below is called a thunk, which can contain both sync and async logic +// that has access to both `dispatch` and `getState`. They can be dispatched like +// a regular action: `dispatch(incrementIfOdd(10))`. // Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { +export const incrementIfOdd = (amount: number): AppThunk => { + return (dispatch, getState) => { const currentValue = selectCount(getState()) - - if (currentValue % 2 === 1 || currentValue % 2 === -1) { + if (currentValue % 2 === 1) { dispatch(incrementByAmount(amount)) } } +} + +// Thunks are commonly used for async logic like fetching data. +// The `createAsyncThunk` method is used to generate thunks that +// dispatch pending/fulfilled/rejected actions based on a promise. +// In this example, we make a mock async request and return the result. +// The `createSlice.extraReducers` field can handle these actions +// and update the state with the results. +export const incrementAsync = createAsyncThunk( + "counter/fetchCount", + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, +) diff --git a/packages/react-native-template-redux-typescript/template/src/features/counter/counterSliceAdvanced.ts b/packages/react-native-template-redux-typescript/template/src/features/counter/counterSliceAdvanced.ts new file mode 100644 index 00000000..420944d9 --- /dev/null +++ b/packages/react-native-template-redux-typescript/template/src/features/counter/counterSliceAdvanced.ts @@ -0,0 +1,93 @@ +// This file has the exact same functionality as `counterSlice.ts`, +// but it uses the newest features from Redux Toolkit 2.0. +// These are optional, but may simplify some of your code. + +import type { PayloadAction } from "@reduxjs/toolkit" +import { createAppSlice } from "../../app/createAppSlice" +import type { AppThunk } from "../../app/store" +import { fetchCount } from "./counterAPI" + +export interface CounterSliceState { + value: number + status: "idle" | "loading" | "failed" +} + +const initialState: CounterSliceState = { + value: 0, + status: "idle", +} + +// If you are not using async thunks you can use the standalone `createSlice`. +export const counterSlice = createAppSlice({ + name: "counter", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: create => ({ + increment: create.reducer(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: create.reducer(state => { + state.value -= 1 + }), + // Use the `PayloadAction` type to declare the contents of `action.payload` + incrementByAmount: create.reducer( + (state, action: PayloadAction) => { + state.value += action.payload + }, + ), + // 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. Thunks are + // typically used to make async requests. + incrementAsync: create.asyncThunk( + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, + { + pending: state => { + state.status = "loading" + }, + fulfilled: (state, action) => { + state.status = "idle" + state.value += action.payload + }, + rejected: state => { + state.status = "failed" + }, + }, + ), + }), + // You can define your selectors here. These selectors receive the slice + // state as their first argument. + selectors: { + selectCount: counter => counter.value, + selectStatus: counter => counter.status, + }, +}) + +// Action creators are generated for each case reducer function. +export const { decrement, increment, incrementByAmount, incrementAsync } = + counterSlice.actions + +// Selectors returned by `slice.selectors` take the root state as their first argument. +export const { selectCount, selectStatus } = counterSlice.selectors + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = + (amount: number): AppThunk => + (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1 || currentValue % 2 === -1) { + dispatch(incrementByAmount(amount)) + } + } diff --git a/packages/vite-template-redux/src/features/counter/Counter.tsx b/packages/vite-template-redux/src/features/counter/Counter.tsx index a286d80c..e8bd9549 100644 --- a/packages/vite-template-redux/src/features/counter/Counter.tsx +++ b/packages/vite-template-redux/src/features/counter/Counter.tsx @@ -1,5 +1,4 @@ import { useState } from "react" - import { useAppDispatch, useAppSelector } from "../../app/hooks" import styles from "./Counter.module.css" import { diff --git a/packages/vite-template-redux/src/features/counter/counterSlice.test.ts b/packages/vite-template-redux/src/features/counter/counterSlice.test.ts index 12eafe1f..ea57b483 100644 --- a/packages/vite-template-redux/src/features/counter/counterSlice.test.ts +++ b/packages/vite-template-redux/src/features/counter/counterSlice.test.ts @@ -1,6 +1,6 @@ import type { AppStore } from "../../app/store" import { makeStore } from "../../app/store" -import type { CounterSliceState } from "./counterSlice" +import type { CounterState } from "./counterSlice" import { counterSlice, decrement, @@ -15,7 +15,7 @@ interface LocalTestContext { describe("counter reducer", it => { beforeEach(context => { - const initialState: CounterSliceState = { + const initialState: CounterState = { value: 3, status: "idle", } diff --git a/packages/vite-template-redux/src/features/counter/counterSlice.ts b/packages/vite-template-redux/src/features/counter/counterSlice.ts index 07bc1f5c..702af72e 100644 --- a/packages/vite-template-redux/src/features/counter/counterSlice.ts +++ b/packages/vite-template-redux/src/features/counter/counterSlice.ts @@ -1,89 +1,100 @@ +// This file demonstrates typical usage of Redux Toolkit's createSlice function +// for defining reducer logic and actions, as well as related thunks and selectors. + import type { PayloadAction } from "@reduxjs/toolkit" -import { createAppSlice } from "../../app/createAppSlice" -import type { AppThunk } from "../../app/store" +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" +import type { AppThunk, RootState } from "../../app/store" import { fetchCount } from "./counterAPI" -export interface CounterSliceState { +// Define the TS type for the counter slice's state +export interface CounterState { value: number status: "idle" | "loading" | "failed" } -const initialState: CounterSliceState = { +// Define the initial value for the slice state +const initialState: CounterState = { value: 0, status: "idle", } -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ +// Slices contain Redux reducer logic for updating state, and +// generate actions that can be dispatched to trigger those updates. +export const counterSlice = createSlice({ name: "counter", - // `createSlice` will infer the state type from the `initialState` argument initialState, // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { + 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: create.reducer(state => { + }, + decrement: state => { state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer( - (state, action: PayloadAction) => { + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: builder => { + builder + // Handle the action types defined by the `incrementAsync` thunk defined below. + // This lets the slice reducer update the state with request status and results. + .addCase(incrementAsync.pending, state => { + state.status = "loading" + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = "idle" state.value += action.payload - }, - ), - // 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. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, + }) + .addCase(incrementAsync.rejected, state => { + state.status = "failed" + }) }, }) -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions +// Export the generated action creators for use in components +export const { increment, decrement, incrementByAmount } = counterSlice.actions -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors +// Export the slice reducer for use in the store configuration +export default counterSlice.reducer -// We can also write thunks by hand, which may contain both sync and async logic. +// Selector functions allows us to select a value from the Redux root state. +// Selectors can also be defined inline in the `useSelector` call +// in a component, or inside the `createSlice.selectors` field. +export const selectCount = (state: RootState) => state.counter.value +export const selectStatus = (state: RootState) => state.counter.status + +// The function below is called a thunk, which can contain both sync and async logic +// that has access to both `dispatch` and `getState`. They can be dispatched like +// a regular action: `dispatch(incrementIfOdd(10))`. // Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { +export const incrementIfOdd = (amount: number): AppThunk => { + return (dispatch, getState) => { const currentValue = selectCount(getState()) - - if (currentValue % 2 === 1 || currentValue % 2 === -1) { + if (currentValue % 2 === 1) { dispatch(incrementByAmount(amount)) } } +} + +// Thunks are commonly used for async logic like fetching data. +// The `createAsyncThunk` method is used to generate thunks that +// dispatch pending/fulfilled/rejected actions based on a promise. +// In this example, we make a mock async request and return the result. +// The `createSlice.extraReducers` field can handle these actions +// and update the state with the results. +export const incrementAsync = createAsyncThunk( + "counter/fetchCount", + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, +) diff --git a/packages/vite-template-redux/src/features/counter/counterSliceAdvanced.ts b/packages/vite-template-redux/src/features/counter/counterSliceAdvanced.ts new file mode 100644 index 00000000..420944d9 --- /dev/null +++ b/packages/vite-template-redux/src/features/counter/counterSliceAdvanced.ts @@ -0,0 +1,93 @@ +// This file has the exact same functionality as `counterSlice.ts`, +// but it uses the newest features from Redux Toolkit 2.0. +// These are optional, but may simplify some of your code. + +import type { PayloadAction } from "@reduxjs/toolkit" +import { createAppSlice } from "../../app/createAppSlice" +import type { AppThunk } from "../../app/store" +import { fetchCount } from "./counterAPI" + +export interface CounterSliceState { + value: number + status: "idle" | "loading" | "failed" +} + +const initialState: CounterSliceState = { + value: 0, + status: "idle", +} + +// If you are not using async thunks you can use the standalone `createSlice`. +export const counterSlice = createAppSlice({ + name: "counter", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: create => ({ + increment: create.reducer(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: create.reducer(state => { + state.value -= 1 + }), + // Use the `PayloadAction` type to declare the contents of `action.payload` + incrementByAmount: create.reducer( + (state, action: PayloadAction) => { + state.value += action.payload + }, + ), + // 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. Thunks are + // typically used to make async requests. + incrementAsync: create.asyncThunk( + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + }, + { + pending: state => { + state.status = "loading" + }, + fulfilled: (state, action) => { + state.status = "idle" + state.value += action.payload + }, + rejected: state => { + state.status = "failed" + }, + }, + ), + }), + // You can define your selectors here. These selectors receive the slice + // state as their first argument. + selectors: { + selectCount: counter => counter.value, + selectStatus: counter => counter.status, + }, +}) + +// Action creators are generated for each case reducer function. +export const { decrement, increment, incrementByAmount, incrementAsync } = + counterSlice.actions + +// Selectors returned by `slice.selectors` take the root state as their first argument. +export const { selectCount, selectStatus } = counterSlice.selectors + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOdd = + (amount: number): AppThunk => + (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1 || currentValue % 2 === -1) { + dispatch(incrementByAmount(amount)) + } + }