Getting Started Redux with React
Hi everyone!
In this article, I want to describe what Redux does in React, its basic features, and how to use it to integrate with React. At last, I implement a simple project using React, Redux, and TypeScript. Hope you enjoy it! 😉
It’s one of the three parts of my React state managers collection:
- Getting Started Redux with React
- Getting Started Recoil with React
- Redux vs. Recoil
Table of Contents
Ok, What does Redux do in React?
As you know, there is a concept named state
in React. In short, Redux is a state management tool with React. It is a library that you can add to your project easily using npm
. Imagine there is a state in the left-down part of the DOM tree in your React project. If you want to use that state in the right-down of the DOM of your project, traditionally you should pass the state and some callbacks to achieve the goal. But using Redux, you just need an import in the right-down component! It’s the benefit and the magic of Redux.
Redux is more general than React projects. You can use it in any JavaScript project (I won’t support this part). So if you want to use it in the React project, you also need the React-Redux library which is the Redux official package.
Redux has some general APIs, but there is also Redux Toolkit which makes creating projects even easier. Redux official documentation about Redux Toolkit says:
Redux Toolkit is our recommended approach for writing Redux logic. It contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
I’ll use React-Redux and Redux Toolkit in this article.
Redux Terminologies
I got help from Redux official documentation to write this part.
Actions
An action is a plain JavaScript object that has a type
field. You can think of an action as an event that describes something that happened in the application.
The type
field should be a string that gives this action a descriptive name, like 'todos/todoAdded'
. We usually write that type string like 'domain/eventName'
, where the first part is the feature or category that this action belongs to, and the second part is the specific thing that happened.
An action object can have other fields with additional information about what happened. By convention, we put that information in a field called payload
. The type of the payload
is dependent on its usage in your project.
A typical action object might look like this:
type actionType = {
type: string,
payload: number
}
const incrementCounterAction: actionType = {
type: 'counter/increment',
payload: 2
}
Action Creators
An action creator is a function that creates and returns an action object. We typically use these so we don’t have to write the action object by hand every time:
type actionType = {
type: string,
payload: number
}
const addTodo = (value: number): actionType => {
return {
type: 'counter/increment',
payload: value
}
}
Reducers
A reducer is a function that receives the current state
and an action
object, decides how to update the state if necessary, and returns the new state: (state, action) => newState
. You can think of a reducer as an event listener which handles events based on the received action (event) type.
Reducers must always follow some specific rules:
- They should only calculate the new state value based on the
state
andaction
arguments. - They are not allowed to modify the existing
state
. Instead, they must make immutable updates, by copying the existingstate
and making changes to the copied values. (Later we’ll see Redux Toolkit’screateSlice
function which lets us mutate a state in a reducer and there, using it is recommended.) - They must not do any asynchronous logic, calculate random values, or cause other “side effects”. (You can achieve asynchronous logic using Thunks. We won’t support that in this article. Read more here.)
The logic inside reducer functions typically follows the same series of steps:
- Check to see if the reducer cares about this action. If so, make a copy of the state, update the copy with new values, and return it.
- Otherwise, return the existing state unchanged
Here’s a small example of a reducer, showing the steps that each reducer should follow:
type actionType = {
type: string
}
type stateType = {
value: number
}
const initialState: stateType = {
value: 0
}
function counterReducer(state: stateType = initialState, action: actionType) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
Slices
There is a concept in Redux named Slice. A slice is a collection of Redux reducer logics and actions for a single feature in your app. The name comes from splitting up the root Redux state object (store) into multiple slices of state.
createSlice
is a function that accepts an initial state, an object of reducer functions, and a slice name, and automatically generates action creators and action types that correspond to the reducers and state.
This API is the standard approach for writing Redux logic.
You can define a slice as follow (complying immutability):
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
return {
...state,
value: state.value + 1
}
},
decrement: state => {
return {
...state,
value: state.value - 1
}
},
incrementByAmount: (state, action: PayloadAction<number>) => {
return {
...state,
value: state.value + action.payload
}
}
}
})
Internally, createSlice
uses createAction
and createReducer
, so you may also use Immer to write mutating immutable updates as below:
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
Please pay attention that the type of action
in the incrementByAmount
function is PayloadAction<number>
which is equal to the following type:
type actionType = {
type: string,
payload: number
}
createSlice
has three main parameters:
name
: A string name for this slice of the state. Generated action type constants (type
field inactionType
) will use this as a prefix. It should be a unique name between all slices otherwise it may produce bugs!initialState
: The initial state value for this slice of the state.reducers
: An object containing Redux case reducer functions (functions intended to handle a specific action type, equivalent to a single case statement in a switch). The keys in the object will be used to generate string action type constants. Also, if any other part of the application happens to dispatch (tell the application that a specific change happened) an action with the same type string, the corresponding reducer will be run.
So there are these actions corresponding to the above slice:
counterSlice.actions.increment()
counterSlice.actions.decrement()
// number 2 is for an example. You can use any other number!
counterSlice.actions.incrementByAmount(2)
or in another way:
const {increment, decrement, incrementByAmount} = counterSlice.actions
And if you want to get its reducer, you can use:
counterSlice.reducer
Store
Any Redux application has just one main state. The state lives in an object called the store
.
The store is created by passing in a reducer, and has a method called getState
that returns the current state value:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
})
console.log(store.getState())
// {counter: {value: 0}}
Or if you have two reducers, it is something like this:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
searchInput: inputSlice.reducer
}
})
console.log(store.getState())
// {counter: {value: 0}, searchInput: {input: "my desired search"}}
As told, Redux has just one main state, named store! Please do not get confused. All states of the app are aggregated in one main state which name is store. Each real React state is named a slice in a Redux app.
Dispatch
The React-Redux library has a custom hook called useDispatch
. The way to update the state and UI is to call useDispatch
and pass in an action object. The Redux’s store will run its corresponding reducer function and save the new state value inside, and we can call getState()
to retrieve the updated value:
import { useDispatch } from "react-redux";
const dispatch = useDispatch()
dispatch(counterSlice.actions.increment())
console.log(store.getState())
// {counter: {value: 1}}
You can think of dispatching actions as “triggering an event” in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.
Selectors
Selectors are functions that know how to extract specific pieces of information from a store state value. As an application grows bigger, this can help avoid repeating logic as different parts of the app need to read the same data. For this purpose, there is a hook called useSelector
. The selector will be called with the entire Redux store state as its only argument. The selector may return any value as a result, including directly returning a value that was nested inside the state
, or deriving new values. The return value of the selector will be used as the return value of the useSelector()
hook:
type RootState = ReturnType<typeof store.getState>
const count = useSelector(state: RootState => {
return state.counter.value
})
RootState
is the type of whole application’s state which is the type of store.getState()
’s return value.
Ok, Great! 🥳
Now you know the basic and main features of Redux in a React project.
Now let’s implement a simple search engine to learn Redux in practice. 🔥
Definition of The Project (Search Engine)
We have some bus Ids ranging from 1 to 10. These buses move from an origin to a destination in a specific day number. We search (filter
) between all available buses and return the bus ids which match the search. I won’t pay attention to the UI and my focus is in Redux. The UI is like this:
Project Introduction
First, the project is available here.
Next, there are some fake results. The type of each result is:
type resultType = {
busId: number,
origin: string,
destination: string,
day: number,
}
And the type of all results is:
type resultsType = resultType[]
Example of a fake result:
const results: resultsType = [
{
"busId": 7,
"origin": "Claremont",
"destination": "Calistoga",
"day": 11
},
{
"busId": 8,
"origin": "Compton",
"destination": "Barstow",
"day": 26
}
]
You can find used fake results here.
There is also a fake results generator which is written in Python if you are interested:
import json
import numpy as np
np.random.seed(42)
cities = ['Alameda', 'Alhambra', 'Anaheim', 'Antioch', 'Arcadia', 'Bakersfield', 'Barstow', 'Belmont', 'Berkeley',
'Beverly Hills', 'Brea', 'Buena Park', 'Burbank', 'Calexico', 'Calistoga', 'Carlsbad', 'Carmel', 'Chico',
'Chula Vista', 'Claremont', 'Compton', 'Concord', 'Corona', 'Coronado', 'Costa Mesa', 'Culver City',
'Daly City']
class Bus:
def __init__(self):
self.busId = np.random.randint(1, 11)
self.origin = np.random.choice(cities)
self.destination = np.random.choice(cities)
self.day = np.random.randint(1, 31)
buses = []
for i in range(100):
buses.append(Bus())
with open('data.json', 'w') as f:
json.dump(buses, f, indent=4, default=lambda x: x.__dict__)
In the continuation of the introduction, our project has two main components, The first one is SearchInput
which is containing our inputs and search button. The next one is SearchResult
which is showing the number of found results and the results, itself.
Preparing a React Project
First, open the directory in which you want to create the project. Then run the below command to create the React project with TypeScript:
npx create-react-app redux-tutorial --template typescript
Then run cd redux-tutorial
to open the project directory. After, run the below commands to install Redux in your project:
npm install @reduxjs/toolkit
npm install react-redux
At the end open the redux-tutorial
directory with any IDE or editor you want.
If you wish to run the project, easily run the below command:
npm start
Implementing The Project
Result
We put our fake results and the corresponding types in the results.ts
file in the redux-tutorial/src/components/searchResult
directory. The defined types are:
export type resultType = {
busId: number,
origin: string,
destination: string,
day: number,
}
export type resultsType = resultType[]
You can download the complete file here.
App
As I said, There are two main components named SearchInput
and SearchResult
. So we change the App
component as below, easily:
import React from 'react';
// import {SearchInput} from "./components/searchInput/SearchInput";
// import {SearchResult} from "./components/searchResult/SearchResult";
function App() {
return (
<div className="App">
{/*<SearchInput/>*/}
{/*<SearchResult/>*/}
</div>
);
}
export default App;
We comment on those lines because those components do not exist, now. We will uncomment them.
Store
We need to define Redux’s only store. It’s better to create a file named store.ts
in redux-tutorial/src/
. So the file redux-tutorial/src/store.ts
is like this:
import {configureStore} from "@reduxjs/toolkit";
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
export const store = configureStore({
reducer: {
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch
The two first lines are imports. Then we configure and create a store
easily.
Because we are writing TypeScript, we need to use typed useSelector
and useDispatch
. So we need RootState
and AppDispatch
types. The RootState
is the type of return value of store.getState()
. The AppDispatch
is the type of store.dispatch
. In the end, using TypedUseSelectorHook
which is defined at react-redux
package, we define useAppSelector
and useAppDispatch
. useAppSelector
is the type of useSelector
and useAppDispatch
is the type of useDispatch
. We’ll use useAppSelector
and useAppDispatch
instead of their originals in our project.
Then open redux-tutorial/src/index.tsx
. We need to wrap theApp
tag in the Provider
tag which is available in the react-redux
package in order to use Redux.
To do so, first import the below lines:
import {Provider} from "react-redux";
import {store} from "./store";
Then this part of the index.tsx
has changed:
root.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>
);
As you see, we need to pass the store
to the Provider
tag. This is needed to use Redux.
Search Input
First, create redux-tutorial/src/components/searchInput
directory. We created a components
folder to have a cleaner project!
Then create SearchInput.tsx
file in the above directory. We need three labels, three inputs for each label, and a search button in it:
import React from "react";
export function SearchInput() {
return (
<div id={'search-input-div'}>
<label htmlFor='origin'>Origin</label>
<input type='text' id='origin'/>
<label htmlFor='destination'>Destination</label>
<input type='text' id='destination'/>
<label htmlFor='day'>Day</label>
<input type='number' min={1} max={30} step={1} id={'day'}/>
<button id={'search-button'}>
Search
</button>
</div>
)
}
Now you can add the SearchInput
component to your App
component (Just uncomment 2 corresponding lines).
Ok, well done. Now we are going to dive into Redux.
One of our React states, or in other words, one of our Redux slices is the input of the origin field. We need to create a slice for it. To do so, we create originInputSlice.ts
file in the same directory. It contains:
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
export const originInputSlice = createSlice({
name: 'originInput',
initialState: '',
reducers: {
setOriginInput: (state, action: PayloadAction<string>) => action.payload
}
})
export const originInputReducer = originInputSlice.reducer
We created a slice, with our desired name. The initial state is an empty string. Pay attention that the type of the initial state is a string which is a primitive type. There’s a point about it. We need just one reducer which gets the string and set the state equal to the string. The input string is embedded at action.payload
. We return the new state, so the state sets with the function return value.
The point is if you use state = action.payload
to set the new state, then the state’s reference changes, so Redux can’t track your change and the new state does not apply. If you do, you may get the A case reducer on a non-draftable value must not return undefined
error.
The other point is if you define the originInputSlice
in SearchInput.tsx
file, you may get Cannot access uninitialized variable
error. So try to define each slice in a separate file.
At last, we export the reducer to use it in the store and we’ll export the originInputSlice
to use its actions. It’s good to see here about exporting and using slices.
The first part of store.ts
changes as below (originInputReducer
added):
import {configureStore} from "@reduxjs/toolkit";
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
import {originInputReducer} from "./components/searchInput/originInputSlice";
export const store = configureStore({
reducer: {
originInput: originInputReducer,
}
})
Let’s go back to the SearchInput.tsx
. We’ll want to use originInputSlice
there. It changes as below:
import React from "react";
import {useAppDispatch, useAppSelector} from "../../store";
import {originInputSlice} from "./originInputSlice";
export function SearchInput() {
const dispatch = useAppDispatch()
const originInput = useAppSelector(state => state.originInput)
return (
<div id={'search-input-div'}>
<label htmlFor='origin'>Origin</label>
<input type='text' id='origin' value={originInput} onChange={e =>
dispatch(originInputSlice.actions.setOriginInput(e.target.value))}/>
<label htmlFor='destination'>Destination</label>
<input type='text' id='destination'/>
<label htmlFor='day'>Day</label>
<input type='number' min={1} max={30} step={1} id={'day'}/>
<button id={'search-button'}>
Search
</button>
</div>
)
}
First, we added dispatch
so we can dispatch our action in the project. Next, we define originInput
to get the user’s input because we want to search using it. Then, obviously, we set the input value equal to originInput
. In the next step, in the onChange
attribute, we run dispatch
on originInputSlice.actions.setOriginInput
action. The input of the action, or in other words, the payload of the action, is the user’s input.
Done! Step into the next Redux Slice.
The next Slice is destinationInputSlice
. Create destinationInputSlice.ts
file in redux-tutorial/src/components/searchInput
directory. The file is:
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
export const destinationInputSlice = createSlice({
name: 'destinationInput',
initialState: {
value: ''
},
reducers: {
setDestinationInput: (state, action: PayloadAction<string>) => {
state.value = action.payload
}
}
})
export const destinationInputReducer = destinationInputSlice.reducer
Overall it’s the same as originInputSlice
. There are some differences between the initial state and its reducer.
Now, the initial state is an object. So we can mutate the state instead of returning the new one. The other approach is to return the new state from the reducer, but this isn’t suggested:
return {
...state,
value: action.payload
}
The last input slice is dayInputSlice
. The redux-tutorial/src/components/searchInput/dayInputSlice.ts
contains:
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
export const dayInputSlice = createSlice({
name: 'dayInput',
initialState: 1,
reducers: {
setDayInput: (state, action: PayloadAction<number>) => action.payload
}
})
export const dayInputReducer = dayInputSlice.reducer
It is the same as two other slices. I use it to just see the use of number
type.
Finally input slices end!
We can add these new slices to the store as below:
export const store = configureStore({
reducer: {
originInput: originInputReducer,
destinationInput: destinationInputReducer,
dayInput: dayInputReducer,
}
})
Let’s come back to the SearchInput.tsx
. It is the same as below until now:
import React from "react";
import {useAppDispatch, useAppSelector} from "../../store";
import {originInputSlice} from "./originInputSlice";
import {destinationInputSlice} from "./destinationInputSlice";
import {dayInputSlice} from "./dayInputSlice";
export function SearchInput() {
const dispatch = useAppDispatch()
const originInput = useAppSelector(state => state.originInput)
const destinationInput = useAppSelector(state => state.destinationInput.value)
const dayInput = useAppSelector(state => state.dayInput)
return (
<div id={'search-input-div'}>
<label htmlFor='origin'>Origin</label>
<input type='text' id='origin' value={originInput} onChange={e =>
dispatch(originInputSlice.actions.setOriginInput(e.target.value))}/>
<label htmlFor='destination'>Destination</label>
<input type='text' id='destination' value={destinationInput} onChange={e =>
dispatch(destinationInputSlice.actions.setDestinationInput(e.target.value))}/>
<label htmlFor='day'>Day</label>
<input type='number' min={1} max={30} step={1} id={'day'} value={dayInput} onChange={e =>
dispatch(dayInputSlice.actions.setDayInput(Number(e.target.value)))}/>
<button id={'search-button'}>
Search
</button>
</div>
)
}
In the file, we added destinationInput
and dayInput
. We also add value
and onChang
attributes for the two other input
tags. The way of adding new codes is as before and there isn’t any new point. Just please pay attention when we want to get destinationInput
because it was an object, we wrote state.destinationInput.value
, not state.destinationInput
!
Now we have just one more slice left. A slice to store search results. create redux-tutorial/src/components/searchResult/searchResultSlice.ts
file. We’ll define searchResultSlice
in it. The file looks like this:
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import {resultsType} from "./results";
export const searchResultSlice = createSlice({
name: 'searchResult',
initialState: [] as resultsType,
reducers: {
setResults: (state: resultsType, action: PayloadAction<resultsType>) => action.payload
}
})
export const searchResultReducer = searchResultSlice.reducer
As you can see, the initial state is empty and its type is resultsType
. The payload
of setResults
reducer is a complete array of results, or in other words, its type as we wrote in the code, is resultsType
. There are two correct approaches to setting the new state and one wrong approach:
- Correct approach 1: Returning the new state. It means returning the
action.payload
same as what we did in the above code. - Wrong approach 1: Setting the state equals to
action.payload
:
setResults: (state: resultsType, action: PayloadAction<resultsType>) => {
state = action.payload
}
As you know this way changes the state reference without returning the new reference. So The change is untraceable and Redux can’t update the UI.
- Correct approach 2: We need to don’t change the reference. So we can delete all items of the state array, and then push the new items in it (This approach is just told for learning, and obviously it’s not good to use.):
setResults: (state: resultsType, action: PayloadAction<resultsType>) => {
for (let i = 0; i < state.length; i++) {
state.pop()
}
for (let i = 0; i < action.payload.length; i++) {
state.push(action.payload[i])
}
}
Now the store.ts
looks like this:
import {configureStore} from "@reduxjs/toolkit";
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
import {originInputReducer} from "./components/searchInput/originInputSlice";
import {destinationInputReducer} from "./components/searchInput/destinationInputSlice";
import {dayInputReducer} from "./components/searchInput/dayInputSlice";
import {searchResultReducer} from "./components/searchResult/searchResultSlice";
export const store = configureStore({
reducer: {
originInput: originInputReducer,
destinationInput: destinationInputReducer,
dayInput: dayInputReducer,
searchResult: searchResultReducer,
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch
The store.ts
completed.
Come back to SearchInput.tsx
again. Two things are left. One of those things is a search function, and the other one is onClick
attribute for the button
tag.
Let’s first implement the search function. It’s so easy. Just a filter on all results as below:
import {results, resultsType} from "../searchResult/results";
function search(originInput: string, destinationInput: string, dayInput: number): resultsType {
return results.filter(result => result.origin.toLowerCase() === originInput.toLowerCase() &&
result.destination.toLowerCase() === destinationInput.toLowerCase() && result.day === dayInput)
}
We search by ignoring case equality between what the user entered and our all results.
Then the onClick
attribute gets a function that dispatches the searchResultSlice.actions.setResults
action. The function is:
() => dispatch(searchResultSlice.actions.setResults(search(originInput, destinationInput, dayInput)))
The input of the action is our search result.
So the final code of SearchInput.tsx
is:
import React from "react";
import {useAppDispatch, useAppSelector} from "../../store";
import {originInputSlice} from "./originInputSlice";
import {destinationInputSlice} from "./destinationInputSlice";
import {dayInputSlice} from "./dayInputSlice";
import {searchResultSlice} from "../searchResult/searchResultSlice";
import {results, resultsType} from "../searchResult/results";
export function SearchInput() {
const dispatch = useAppDispatch()
const originInput = useAppSelector(state => state.originInput)
const destinationInput = useAppSelector(state => state.destinationInput.value)
const dayInput = useAppSelector(state => state.dayInput)
function search(originInput: string, destinationInput: string, dayInput: number): resultsType {
return results.filter(result => result.origin.toLowerCase() === originInput.toLowerCase() &&
result.destination.toLowerCase() === destinationInput.toLowerCase() && result.day === dayInput)
}
return (
<div id={'search-input-div'}>
<label htmlFor='origin'>Origin</label>
<input type='text' id='origin' value={originInput} onChange={e =>
dispatch(originInputSlice.actions.setOriginInput(e.target.value))}/>
<label htmlFor='destination'>Destination</label>
<input type='text' id='destination' value={destinationInput} onChange={e =>
dispatch(destinationInputSlice.actions.setDestinationInput(e.target.value))}/>
<label htmlFor='day'>Day</label>
<input type='number' min={1} max={30} step={1} id={'day'} value={dayInput} onChange={e =>
dispatch(dayInputSlice.actions.setDayInput(Number(e.target.value)))}/>
<button id={'search-button'} onClick={() =>
dispatch(searchResultSlice.actions.setResults(search(originInput, destinationInput, dayInput)))}>
Search
</button>
</div>
)
}
Hooray! This part is finished. Let’s step into Search Result.
Search Result
First, create redux-tutorial/src/components/searchInput/SearchResult.tsx
file. We want to show our results in a form of a table.
Then we need our results here, so we’ll use useAppSelector
to get the results:
import React from "react";
import {useAppSelector} from "../../store";
import {resultsType} from "./results";
export function SearchResult() {
const searchResults: resultsType = useAppSelector(state => state.searchResult)
return (
<div id={'search-result-div'}>
<table>
<thead>
<tr>
<th>Bus Id</th>
<th>Origin</th>
<th>Destination</th>
<th>Day</th>
</tr>
</thead>
<tbody>
{searchResults.map((result, index) => {
return (
<tr key={index}>
<td>{result.busId}</td>
<td>{result.origin}</td>
<td>{result.destination}</td>
<td>{result.day}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
We get our results in the searchResults
variable. Then we show the results in a table to be more beautiful and informative.
Now you can add the SearchResult
component to your App
component (Just uncomment 2 corresponding lines).
I want to have the number of found results in a variable named numberOfResults
. There are two approaches:
- We can use another
useAppSelector
same as below:
const numberOfResults = useAppSelector(state => state.searchResult.length)
This approach is used when we have the same complex logic many times in our code.
- We also can use the
length
attribute on thesearchResults
variable:
const numberOfResults = searchResults.length
So, at last SearchResult.tsx
is:
import React from "react";
import {useAppSelector} from "../../store";
import {resultsType} from "./results";
export function SearchResult() {
const searchResults: resultsType = useAppSelector(state => state.searchResult)
const numberOfResults = useAppSelector(state => state.searchResult.length)
return (
<div id={'search-result-div'}>
<h2>{numberOfResults} results found</h2>
<table>
<thead>
<tr>
<th>Bus Id</th>
<th>Origin</th>
<th>Destination</th>
<th>Day</th>
</tr>
</thead>
<tbody>
{searchResults.map((result, index) => {
return (
<tr key={index}>
<td>{result.busId}</td>
<td>{result.origin}</td>
<td>{result.destination}</td>
<td>{result.day}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
We added numberOfResults
as a h2
tag.
CSS
If you want some CSS to make your search engine more beautiful, you can download App.css
, SearchInput.css
, and SearchResult.css
stylesheets, and import them to App.tsx
, SearchInput.tsx
, and SearchResult.tsx
, respectively.
Final Word
Now, You have a beautiful and powerful search engine. 🎉🔥
If you want to get deep into Redux, you can use Redux’s official document.
Hope you enjoyed this journey and learned Redux very well. If you have any problem or suggestion, I’ll get so happy if I hear from you.