Understanding the Counter Application
This chapter will be mostly focused on looking at and understanding code. The explanations will be limited to the reader, then look at the code themselves and make connections based on what we learned in the previous chapters.
This chapter will look at the Counter app with the Redux template. When we run the default application, we see the following screen:
Following is the file structure of the Redux app highlighting the files we want to look at:
src
app
store.js
features
counter
counter.js
counterAPI.js
counterSlice.js
App.js
index.js
We will first look at the store.js
file to get an idea of the structure of the Redux store:
js912345export const store = configureStore({reducer: {counter: counterReducer,},});
It has a single slice and is handled by the reducer counterReducer
. Let’s look at the sliced code inside the counterSlice.js
file:
js99123456789101112131415161718192021222324252627282930313233343536import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';import { fetchCount } from './counterAPI';const initialState = {value: 0,status: 'idle',};// 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.export const incrementAsync = createAsyncThunk('counter/fetchCount',async (amount) => {const response = await fetchCount(amount);// The value we return becomes the `fulfilled` action payloadreturn response.data;});export const counterSlice = createSlice({name: 'counter',initialState,// The `reducers` field lets us define reducers and generate associated actionsreducers: {increment: (state) => {// Redux Toolkit allows us to write "mutating" logic in reducers. It// doesn't mutate the state because it uses the Immer library,// which detects changes to a "draft state" and produces a brand new// immutable state based on those changesstate.value += 1;},decrement: (state) => {state.value -= 1;
The initial state is set to the following:
js91234const initialState = {value: 0,status: 'idle',};
It has reducers, namely increment
, decrement
, and incrementByAmount
. It also has two thunk functions. The first thunk function is the following:
js912345678export const incrementAsync = createAsyncThunk('counter/fetchCount',async (amount) => {const response = await fetchCount(amount);// The value we return becomes the `fulfilled` action payloadreturn response.data;});
It fetches data from a pseudo-API in the counterAPI
file. It is nothing but a timer that delays sending the data by 500 ms:
js912345export function fetchCount(amount = 1) {return new Promise((resolve) =>setTimeout(() => resolve({ data: amount }), 500));}
The following reducers are handling it:
js9912345678910extraReducers: (builder) => {builder.addCase(incrementAsync.pending, (state) => {state.status = 'loading';}).addCase(incrementAsync.fulfilled, (state, action) => {state.status = 'idle';state.value += action.payload;});}
While it's waiting for the data, the status
key is set to loading
and when it is done, it is set back to idle
.
The second thunk function simply dispatches another action based on the current state, conditionally:
js9123456export const incrementIfOdd = (amount) => (dispatch, getState) => {const currentValue = selectCount(getState());if (currentValue % 2 === 1) {dispatch(incrementByAmount(amount));}};
There is also a selector being exported from the file:
jsexport const selectCount = (state) => state.counter.value;
Now that we know what's in the slice, let’s look at the entry point of the application index.js
:
js991234567891011121314151617181920212223import React from 'react';import { createRoot } from 'react-dom/client';import { Provider } from 'react-redux';import { store } from './app/store';import App from './App';import reportWebVitals from './reportWebVitals';import './index.css';const container = document.getElementById('root');const root = createRoot(container);root.render(<React.StrictMode><Provider store={store}><App /></Provider></React.StrictMode>);// If you want to start measuring performance in your app, pass a function// to log results (for example reportWebVitals(console.log))// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitalsreportWebVitals();
We enclose the top-level component into <Provider>
tags and set the store
attribute to the imported store.
The top-level component is from the App.js
file:
js99123456789101112131415161718192021222324252627282930313233343536import React from 'react';import logo from './logo.svg';import { Counter } from './features/counter/Counter';import './App.css';function App() {return (<div className="App"><header className="App-header"><img src={logo} className="App-logo" alt="logo" /><Counter /><p>Edit <code>src/App.js</code> and save to reload.</p><span><span>Learn </span><aclassName="App-link"href="https://reactjs.org/"target="_blank"rel="noopener noreferrer">React</a><span>, </span><aclassName="App-link"href="https://redux.js.org/"target="_blank"rel="noopener noreferrer">Redux</a><span>, </span><aclassName="App-link"
The top-level components have the following elements:
In the start, the actions, thunks, and selector has been imported. Along with that, the relevant functions from react-redux
:
js9123456789import { useSelector, useDispatch } from 'react-redux';import {decrement,increment,incrementByAmount,incrementAsync,incrementIfOdd,selectCount,} from './counterSlice';
The selector has been set to the count
constant in the following lines. You will also notice a local state variable, incrementAmount
. We don’t want to store this variable in the Redux store since we don’t want to make this state value accessible from other components.
js9123const count = useSelector(selectCount);const dispatch = useDispatch();const [incrementAmount, setIncrementAmount] = useState('2');
In the following section, we use the selector to display the count and use the dispatch
function to dispatch relevant actions on button clicks for decrementing or incrementing the count:
js991234567891011121314151617<div className={styles.row}><buttonclassName={styles.button}aria-label="Decrement value"onClick={() => dispatch(decrement())}>-</button><span className={styles.value}>{count}</span><buttonclassName={styles.button}aria-label="Increment value"onClick={() => dispatch(increment())}>+</button></div>
In the next <div>
, we have buttons for incrementing by amount. In this case, we also pass arguments into the dispatch actions, which go into the payload
:
js991234567891011121314151617181920212223242526<div className={styles.row}><inputclassName={styles.textbox}aria-label="Set increment amount"value={incrementAmount}onChange={(e) => setIncrementAmount(e.target.value)}/><buttonclassName={styles.button}onClick={() => dispatch(incrementByAmount(incrementValue))}>Add Amount</button><buttonclassName={styles.asyncButton}onClick={() => dispatch(incrementAsync(incrementValue))}>Add Async</button><buttonclassName={styles.button}onClick={() => dispatch(incrementIfOdd(incrementValue))}>Add If Odd</button></div>
Recall that payload
is a key inside the action
argument, which can be accessed from the reducers.
Obrigado pelo seu feedback!
Pergunte à IA
Pergunte o que quiser ou experimente uma das perguntas sugeridas para iniciar nosso bate-papo