DEV Community

Yuqing Ma
Yuqing Ma

Posted on • Updated on

How to understand example from React Redux from TypeScript?

How to understand example from React Redux from TypeScript?
Introduction:
Redux is essential for data processing in React development and TypeScript is getting popular. The document also recommends using TypeScript. For beginner of those concepts, the original document is a little bit challenging. Here, I make some extra notes for the Typescript example.
Sample from https://redux-toolkit.js.org/tutorials/typescript
Main:
A Store is a place where the entire state of your application lists. It manages the status of the application and has a dispatch(action) function. It is like a brain responsible for all moving parts in Redux. So you can think that store centrally holds the data in memory.

Image description

  1. Component is fundamental for React, so many concepts can be explained from concept of components. This code comes from index.tsx. <Provider store={store}> <App /> </Provider> You can consider store is passed to root component as props. That is the reason that all the other child components can use store directly. Then we start looks at the store.ts in src/app folder. `import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
reducer: {
counter: counterReducer,
},
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType;
export type AppThunk = ThunkAction<
ReturnType,
RootState,
unknown,
Action

;
_From Object Oriented Programming, we can consider store is an object from configureStore class. We will talk about counterSlice later. Then we look at the type of AppDispatch and RootState.typeof store.dispatch returns a type of store.dispatch and ReturnType<typeof store.getState> also return the type of getState. _
Here it is an example of ReturnType<typeof > with concept of generics
function sendData(a: number, b: number) {
return {
a: ${a},
b: ${b}
}
}
type Data = ReturnType
// The same as writing:
// type Data = {
// a: string,
// b: string
// }`
We should understand an idea Type Assertion in typescript.
Angle Brackets <> is called Type Assertion or casting.
Example:

`let code: any = 123;
let employeeCode = code;
console.log(typeof(employeeCode)); //Output: number

let employee = { };
employee.name = "John"; //Compiler Error: Property 'name' does not exist on type '{}'
employee.code = 123; //Compiler Error: Property 'code' does not exist on type '{}'

interface Employee {
name: string;
code: number;
}

let employee = { };
employee.name = "John"; // OK
employee.code = 123; // OK
Here it is the explaination for ThunkAction
We can consider it as utility types in Typescript.
export type AppThunk = ThunkAction<
ReturnType,
RootState,
unknown,
Action

;`
Then here is the blog to explain ThunkAction
https://bloggie.io/@_ChristineOo/understanding-typings-of-redux-thunk-action

`import { AnyAction } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage =
(message: string): ThunkAction =>
async dispatch => {
const asyncResp = await exampleAPI()
dispatch(
sendMessage({
message,
user: asyncResp,
timestamp: new Date().getTime()
})
)
}

export type ThunkAction<
R, // Return type of the thunk function
S, // state type used by getState
E, // any "extra argument" injected into the thunk
A extends Action // known types of actions that can be dispatched

= (dispatch: ThunkDispatch, getState: () => S, extraArgument: E) => R
Redux Thunk is the standard middleware for writing sync and async logic that interacts with the Redux store. A thunk function receives dispatch and getState as its parameters. Redux Thunk has a built in ThunkAction type which we can use to define types for those arguments:
function exampleAPI() {
return Promise.resolve('Async Chat Bot')
}
Example of using ThunkAction directly
import { AnyAction } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage =
(message: string): ThunkAction =>
async dispatch => {
const asyncResp = await exampleAPI()
dispatch(
sendMessage({
message,
user: asyncResp,
timestamp: new Date().getTime()
})
)
}

function exampleAPI() {
return Promise.resolve('Async Chat Bot')
}
`
To reduce repetition, you might want to define a reusable AppThunk type once, in your store file, and then use that type whenever you write a thunk:
export type AppThunk = ThunkAction<
ReturnType,
RootState,
unknown,
AnyAction

In src/features/counter/counterSlice.ts file
Define the state of Counter and pass it to initialState.
`export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}

const initialState: CounterState = {
value: 0,
status: 'idle',
};
Inside counterSlice object, name:’counter’ gives a name for your actions. If the name is ‘counter’ then the updateCounter action will have {type: ‘counter/updateCounter’} . If it is ‘abc’ then your action will have {type:’abc/updateCounter’}. The name should be distinguished from other reducers’ names.
In the const selectCount = (state: RootState) => state.counter.value; the counter is not the name counter in counterSlice object. This selector function assumes that the reducer from the slice will be on the counter property of your root reducer. So in store.ts file this code :
export const store = configureStore({
reducer: {
counter: counterReducer,
},
}); `
It defines the counter in selectCount. The location of the reducer relative to the root state is determined by the property key when you combine your reducers with configureStore or combineReducers.

In redux, the reducers are the pure functions that contain the logic and calculation that needed to be performed on the state.

We need to discuss what extraReducers is. The extraReducers field lets the slice handle actions defined elsewhere, including actions generated by createAsyncThunk or in other slices.
Builder in action reduer is a type-safe boundary around unsafe code.
addCase will run the reducer if the dispatched action's action.type field exactly matches the string that you provide
const reducer = createReducer(0, {
"increment": (state) => state + 1,
"set": (state, action: PayloadAction<number>) => action.payload
});
It does the same result as follow:
const reducer = createReducer(0, (builder => builder
.addCase(incrementAction, (state) => state + 1)
.addCase(setAction, (state, action) => action.payload)
));

incrementAsync is an object of createAsyncThunk class so it has type parameters:
A string that will be used to generate additional Redux action type constants, representing the lifecycle of an async request:
For example, a type argument of 'users/requestStatus' will generate these action types:
pending: 'users/requestStatus/pending'
fulfilled: 'users/requestStatus/fulfilled'
rejected: 'users/requestStatus/rejected'
createSlice will return an object that looks like:
{
name : string,
reducer : ReducerFunction,
actions : Record,
caseReducers: Record.
getInitialState: () => State
}
Each function defined in the reducers argument will have a corresponding action creator generated using createAction and included in the result's actions field using the same function name.
That is why we have export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
The : is also used in typescript to define an objects type.
It is different from function types expression in Typescript

Top comments (0)