iTranslated by AI
My First React & Redux Project
Introduction
I studied redux and created a very simple project!
Until now, for React projects, I've been using create-react-app, but this time I set up the environment with webpack. I'm also planning to study webpack and write an article about it.
It's available on github, so please check it out.
Project Overview
The project, named "Voting Box", is a feature that allows users to add or subtract votes, and check individual vote counts as well as the total vote count.
File Structure
├── src/
├── components/
└──Counter.module.css
└──Counter.tsx
└──action.ts
└──App.module.css
└──App.tsx
└──index.html
└──index.tsx
└──reducer.ts
Top-level index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { counterReducer, initialState } from './reducer';
const store = createStore(counterReducer, initialState);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
);
To use Redux, you need to wrap the App component with a Provider component that passes the store as props.
The store must be created and initialized with the createStore function. This function requires reducer and the initial state as arguments. I will explain this further when we look at the reducer.ts file.
Action to change the store's state
export const ActionType = {
DECREMENT: 'DECREMENT',
INCREMENT: 'INCREMENT',
} as const;
type ValueOf<T> = T[keyof T>;
export type Action = {
type: ValueOf<typeof ActionType>;
amount?: number;
};
export const decrement = (): Action => ({
type: ActionType.DECREMENT,
});
export const increment = (): Action => ({
type: ActionType.INCREMENT,
});
The role of an action is to create an object that distinguishes between event types.
This time, the necessary events are increment and decrement, so we define them in ActionType.
The object created by an action requires a type property to store the event type and an amount property to store the value to be added. Therefore, we define the object's type as type Action.
Hovering over the type property will show a union type of increment and decrement, as shown below.
Finally, at the end of action.ts, functions to generate actions are defined. These functions change the state of the store.
Reducer to generate new state
import { Reducer } from 'redux';
import { Action, ActionType as Type } from './action';
export type CounterState = {
count: number;
};
export const initialState: CounterState = { count: 0 };
export const counterReducer: Reducer<CounterState, Action> = (
state: CounterState = initialState,
action: Action
): CounterState => {
switch (action.type) {
case Type.DECREMENT:
return {
...state,
count: state.count - 1,
};
case Type.INCREMENT:
return {
...state,
count: state.count + 1,
};
default:
const check: never = action.type;// Prevent omission of case statements due to action.type
return state;
}
};
The role of a reducer is to receive the current state and an action, and then create a new state.
First, we define the type of the state and the initial value of the state, respectively.
The counterReducer then represents a function that takes the current state and an action as arguments, and returns a new state based on the event type defined in the action.
We have now seen the reducer and the initial state of the state that were passed as arguments to the createStore function in index.tsx.
By the way, if you hover over Reducer in counterReducer (where generic types are used for type definition) and press F12, you can check the type definition in index.d.ts.
Implement Redux functionality on the component side
import React from 'react';
import { useSelector } from 'react-redux';
import { CounterState } from './reducer';
import Counter from './components/Counter';
import AppCSS from './App.module.css';
type VoteManga = string[];
const App = () => {
const count = useSelector<CounterState, number>((state) => state.count);
const votesManga: VoteManga = ['Aさん', 'Bさん', 'Cさん'];
return (
<div className={AppCSS.container}>
<div className={AppCSS.header}>
<h1>投票箱</h1>
<h2>Total Votes:{count}</h2>
</div>
{votesManga.map((vote, i) => (
<Counter key={i} name={vote} />
))}
</div>
);
};
export default App;
In the definition of the count variable, useSelector, a Redux Hook, is used. useSelector allows you to get the state from the store. Here, we've implemented the functionality to check the total number of votes.
This time, the state defined in the reducer only had count, but in a large-scale project, there would be multiple types of state, and the idea is to use useSelector to retrieve the necessary store state for each component.
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { increment, decrement } from '../action';
import CounterCSS from './Counter.module.css';
interface Props {
name: string;
}
const Counter: React.FC<Props> = ({ name }) => {
const dispatch = useDispatch();
const [votes, setVotes] = useState<number>(0);
const handleDecrement = () => {
dispatch(decrement());
setVotes(votes - 1);
};
const handleIncrement = () => {
dispatch(increment());
setVotes(votes + 1);
};
return (
<div className={CounterCSS.header}>
<h2>{name}</h2>
<h3>{`Votes: ${votes}`}</h3>
<div>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
</div>
);
};
export default Counter;
In the definition of the dispatch variable, useDispatch, a Redux Hook, is used. By passing an action as an argument to this dispatch variable, each event's action is dispatched. Here, we've implemented the functionality to check, add, and reduce individual vote counts.
That's all. Thank you for reading!
Discussion