mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
467 lines
18 KiB
Markdown
467 lines
18 KiB
Markdown
|
[#]: subject: (Build a to-do list app in React with hooks)
|
|||
|
[#]: via: (https://opensource.com/article/21/3/react-app-hooks)
|
|||
|
[#]: author: (Jaivardhan Kumar https://opensource.com/users/invinciblejai)
|
|||
|
[#]: collector: (lujun9972)
|
|||
|
[#]: translator: ( )
|
|||
|
[#]: reviewer: ( )
|
|||
|
[#]: publisher: ( )
|
|||
|
[#]: url: ( )
|
|||
|
|
|||
|
Build a to-do list app in React with hooks
|
|||
|
======
|
|||
|
Learn to build React apps using functional components and state
|
|||
|
management.
|
|||
|
![Team checklist and to dos][1]
|
|||
|
|
|||
|
React is one of the most popular and simple JavaScript libraries for building user interfaces (UIs) because it allows you to create reusable UI components.
|
|||
|
|
|||
|
Components in React are independent, reusable pieces of code that serve as building blocks for an application. React functional components are JavaScript functions that separate the presentation layer from the business logic. According to the [React docs][2], a simple, functional component can be written like:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
function Welcome(props) {
|
|||
|
return <h1>Hello, {props.name}</h1>;
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
React functional components are stateless. Stateless components are declared as functions that have no state and return the same markup, given the same props. State is managed in components with hooks, which were introduced in React 16.8. They enable the management of state and the lifecycle of functional components. There are several built-in hooks, and you can also create custom hooks.
|
|||
|
|
|||
|
This article explains how to build a simple to-do app in React using functional components and state management. The complete code for this app is available on [GitHub][3] and [CodeSandbox][4]. When you're finished with this tutorial, the app will look like this:
|
|||
|
|
|||
|
![React to-do list][5]
|
|||
|
|
|||
|
(Jaivardhan Kumar, [CC BY-SA 4.0][6])
|
|||
|
|
|||
|
### Prerequisites
|
|||
|
|
|||
|
* To build locally, you must have [Node.js][7] v10.16 or higher, [yarn][8] v1.20.0 or higher, and npm 5.6
|
|||
|
* Basic knowledge of JavaScript
|
|||
|
* Basic understanding of React would be a plus
|
|||
|
|
|||
|
|
|||
|
|
|||
|
### Create a React app
|
|||
|
|
|||
|
[Create React App][9] is an environment that allows you to start building a React app. Along with this tutorial, I used a TypeScript template for adding static type definitions. [TypeScript][10] is an open source language that builds on JavaScript:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
`npx create-react-app todo-app-context-api --template typescript`
|
|||
|
```
|
|||
|
|
|||
|
[npx][11] is a package runner tool; alternatively, you can use [yarn][12]:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
`yarn create react-app todo-app-context-api --template typescript`
|
|||
|
```
|
|||
|
|
|||
|
After you execute this command, you can navigate to the directory and run the app:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
cd todo-app-context-api
|
|||
|
yarn start
|
|||
|
```
|
|||
|
|
|||
|
You should see the starter app and the React logo which is generated by boilerplate code. Since you are building your own React app, you will be able to modify the logo and styles to meet your needs.
|
|||
|
|
|||
|
### Build the to-do app
|
|||
|
|
|||
|
The to-do app can:
|
|||
|
|
|||
|
* Add an item
|
|||
|
* List items
|
|||
|
* Mark items as completed
|
|||
|
* Delete items
|
|||
|
* Filter items based on status (e.g., completed, all, active)
|
|||
|
|
|||
|
|
|||
|
|
|||
|
![To-Do App architecture][13]
|
|||
|
|
|||
|
(Jaivardhan Kumar, [CC BY-SA 4.0][6])
|
|||
|
|
|||
|
#### The header component
|
|||
|
|
|||
|
Create a directory called **components** and add a file named **Header.tsx**:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
mkdir components
|
|||
|
cd components
|
|||
|
vi Header.tsx
|
|||
|
```
|
|||
|
|
|||
|
Header is a functional component that holds the heading:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
const Header: React.FC = () => {
|
|||
|
return (
|
|||
|
<div className="header">
|
|||
|
<h1>
|
|||
|
Add TODO List!!
|
|||
|
</h1>
|
|||
|
</div>
|
|||
|
)
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
#### The AddTodo component
|
|||
|
|
|||
|
The **AddTodo** component contains a text box and a button. Clicking the button adds an item to the list.
|
|||
|
|
|||
|
Create a directory called **todo** under the **components** directory and add a file named **AddTodo.tsx**:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
mkdir todo
|
|||
|
cd todo
|
|||
|
vi AddTodo.tsx
|
|||
|
```
|
|||
|
|
|||
|
AddTodo is a functional component that accepts props. Props allow one-way passing of data, i.e., only from parent to child components:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
const AddTodo: React.FC<AddTodoProps> = ({ todoItem, updateTodoItem, addTaskToList }) => {
|
|||
|
const submitHandler = (event: SyntheticEvent) => {
|
|||
|
event.preventDefault();
|
|||
|
addTaskToList();
|
|||
|
}
|
|||
|
return (
|
|||
|
<form className="addTodoContainer" onSubmit={submitHandler}>
|
|||
|
<div className="controlContainer">
|
|||
|
<input className="controlSpacing" style={{flex: 1}} type="text" value={todoItem?.text ?? ''} onChange={(ev) => updateTodoItem(ev.target.value)} placeholder="Enter task todo ..." />
|
|||
|
<input className="controlSpacing" style={{flex: 1}} type="submit" value="submit" />
|
|||
|
</div>
|
|||
|
<div>
|
|||
|
<label>
|
|||
|
<span style={{ color: '#ccc', padding: '20px' }}>{todoItem?.text}</span>
|
|||
|
</label>
|
|||
|
</div>
|
|||
|
</form>
|
|||
|
)
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
You have created a functional React component called **AddTodo** that takes props provided by the parent function. This makes the component reusable. The props that need to be passed are:
|
|||
|
|
|||
|
* **todoItem:** An empty item state
|
|||
|
* **updateToDoItem:** A helper function to send callbacks to the parent as the user types
|
|||
|
* **addTaskToList:** A function to add an item to a to-do list
|
|||
|
|
|||
|
|
|||
|
|
|||
|
There are also some styling and HTML elements, like form, input, etc.
|
|||
|
|
|||
|
#### The TodoList component
|
|||
|
|
|||
|
The next component to create is the **TodoList**. It is responsible for listing the items in the to-do state and providing options to delete and mark items as complete.
|
|||
|
|
|||
|
**TodoList** will be a functional component:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
const TodoList: React.FC = ({ listData, removeItem, toggleItemStatus }) => {
|
|||
|
return listData.length > 0 ? (
|
|||
|
<div className="todoListContainer">
|
|||
|
{ listData.map((lData) => {
|
|||
|
return (
|
|||
|
<ul key={lData.id}>
|
|||
|
<li>
|
|||
|
<div className="listItemContainer">
|
|||
|
<input type="checkbox" style={{ padding: '10px', margin: '5px' }} onChange={() => toggleItemStatus(lData.id)} checked={lData.completed}/>
|
|||
|
<span className="listItems" style={{ textDecoration: lData.completed ? 'line-through' : 'none', flex: 2 }}>{lData.text}</span>
|
|||
|
<button type="button" className="listItems" onClick={() => removeItem(lData.id)}>Delete</button>
|
|||
|
</div>
|
|||
|
</li>
|
|||
|
</ul>
|
|||
|
)
|
|||
|
})}
|
|||
|
</div>
|
|||
|
) : (<span> No Todo list exist </span >)
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
The **TodoList** is also a reusable functional React component that accepts props from parent functions. The props that need to be passed are:
|
|||
|
|
|||
|
* **listData:** A list of to-do items with IDs, text, and completed properties
|
|||
|
* **removeItem:** A helper function to delete an item from a to-do list
|
|||
|
* **toggleItemStatus:** A function to toggle the task status from completed to not completed and vice versa
|
|||
|
|
|||
|
|
|||
|
|
|||
|
There are also some styling and HTML elements (like lists, input, etc.).
|
|||
|
|
|||
|
#### Footer component
|
|||
|
|
|||
|
**Footer** will be a functional component; create it in the **components** directory as follows:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
cd ..
|
|||
|
|
|||
|
const Footer: React.FC = ({item = 0, storage, filterTodoList}) => {
|
|||
|
return (
|
|||
|
<div className="footer">
|
|||
|
<button type="button" style={{flex:1}} onClick={() => filterTodoList(ALL_FILTER)}>All Item</button>
|
|||
|
<button type="button" style={{flex:1}} onClick={() => filterTodoList(ACTIVE_FILTER)}>Active</button>
|
|||
|
<button type="button" style={{flex:1}} onClick={() => filterTodoList(COMPLETED_FILTER)}>Completed</button>
|
|||
|
<span style={{color: '#cecece', flex:4, textAlign: 'center'}}>{item} Items | Make use of {storage} to store data</span>
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
It accepts three props:
|
|||
|
|
|||
|
* **item:** Displays the number of items
|
|||
|
* **storage:** Displays text
|
|||
|
* **filterTodoList:** A function to filter tasks based on status (active, completed, all items)
|
|||
|
|
|||
|
|
|||
|
|
|||
|
### Todo component: Managing state with contextApi and useReducer
|
|||
|
|
|||
|
![Todo Component][14]
|
|||
|
|
|||
|
(Jaivardhan Kumar, [CC BY-SA 4.0][6])
|
|||
|
|
|||
|
Context provides a way to pass data through the component tree without having to pass props down manually at every level. **ContextApi** and **useReducer** can be used to manage state by sharing it across the entire React component tree without passing it as a prop to each component in the tree.
|
|||
|
|
|||
|
Now that you have the AddTodo, TodoList, and Footer components, you need to wire them.
|
|||
|
|
|||
|
Use the following built-in hooks to manage the components' state and lifecycle:
|
|||
|
|
|||
|
* **useState:** Returns the stateful value and updater function to update the state
|
|||
|
* **useEffect:** Helps manage lifecycle in functional components and perform side effects
|
|||
|
* **useContext:** Accepts a context object and returns current context value
|
|||
|
* **useReducer:** Like useState, it returns the stateful value and updater function, but it is used instead of useState when you have complex state logic (e.g., multiple sub-values or if the new state depends on the previous one)
|
|||
|
|
|||
|
|
|||
|
|
|||
|
First, use **contextApi** and **useReducer** hooks to manage the state. For separation of concerns, add a new directory under **components** called **contextApiComponents**:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
mkdir contextApiComponents
|
|||
|
cd contextApiComponents
|
|||
|
```
|
|||
|
|
|||
|
Create **TodoContextApi.tsx**:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
const defaultTodoItem: TodoItemProp = { id: Date.now(), text: '', completed: false };
|
|||
|
|
|||
|
const TodoContextApi: React.FC = () => {
|
|||
|
const { state: { todoList }, dispatch } = React.useContext(TodoContext);
|
|||
|
const [todoItem, setTodoItem] = React.useState(defaultTodoItem);
|
|||
|
const [todoListData, setTodoListData] = React.useState(todoList);
|
|||
|
|
|||
|
React.useEffect(() => {
|
|||
|
setTodoListData(todoList);
|
|||
|
}, [todoList])
|
|||
|
|
|||
|
const updateTodoItem = (text: string) => {
|
|||
|
setTodoItem({
|
|||
|
id: Date.now(),
|
|||
|
text,
|
|||
|
completed: false
|
|||
|
})
|
|||
|
}
|
|||
|
const addTaskToList = () => {
|
|||
|
dispatch({
|
|||
|
type: ADD_TODO_ACTION,
|
|||
|
payload: todoItem
|
|||
|
});
|
|||
|
setTodoItem(defaultTodoItem);
|
|||
|
}
|
|||
|
const removeItem = (id: number) => {
|
|||
|
dispatch({
|
|||
|
type: REMOVE_TODO_ACTION,
|
|||
|
payload: { id }
|
|||
|
})
|
|||
|
}
|
|||
|
const toggleItemStatus = (id: number) => {
|
|||
|
dispatch({
|
|||
|
type: UPDATE_TODO_ACTION,
|
|||
|
payload: { id }
|
|||
|
})
|
|||
|
}
|
|||
|
const filterTodoList = (type: string) => {
|
|||
|
const filteredList = FilterReducer(todoList, {type});
|
|||
|
setTodoListData(filteredList)
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return (
|
|||
|
<>
|
|||
|
<AddTodo todoItem={todoItem} updateTodoItem={updateTodoItem} addTaskToList={addTaskToList} />
|
|||
|
<TodoList listData={todoListData} removeItem={removeItem} toggleItemStatus={toggleItemStatus} />
|
|||
|
<Footer item={todoListData.length} storage="Context API" filterTodoList={filterTodoList} />
|
|||
|
</>
|
|||
|
)
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
This component includes the **AddTodo**, **TodoList**, and **Footer** components and their respective helper and callback functions.
|
|||
|
|
|||
|
To manage the state, it uses **contextApi**, which provides state and dispatch methods, which, in turn, updates the state. It accepts a context object. (You will create the provider for the context, called **contextProvider**, next).
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
` const { state: { todoList }, dispatch } = React.useContext(TodoContext);`
|
|||
|
```
|
|||
|
|
|||
|
#### TodoProvider
|
|||
|
|
|||
|
Add **TodoProvider**, which creates **context** and uses a **useReducer** hook. The **useReducer** hook takes a reducer function along with the initial values and returns state and updater functions (dispatch).
|
|||
|
|
|||
|
* Create the context and export it. Exporting it will allow it to be used by any child component to get the current state using the hook **useContext**: [code]`export const TodoContext = React.createContext({} as TodoContextProps);`
|
|||
|
```
|
|||
|
* Create **ContextProvider** and export it: [code] const TodoProvider : React.FC = (props) => {
|
|||
|
const [state, dispatch] = React.useReducer(TodoReducer, {todoList: []});
|
|||
|
const value = {state, dispatch}
|
|||
|
return (
|
|||
|
<TodoContext.Provider value={value}>
|
|||
|
{props.children}
|
|||
|
</TodoContext.Provider>
|
|||
|
)
|
|||
|
}
|
|||
|
```
|
|||
|
* Context data can be accessed by any React component in the hierarchy directly with the **useContext** hook if you wrap the parent component (e.g., **TodoContextApi**) or the app itself with the provider (e.g., **TodoProvider**): [code] <TodoProvider>
|
|||
|
<TodoContextApi />
|
|||
|
</TodoProvider>
|
|||
|
```
|
|||
|
* In the **TodoContextApi** component, use the **useContext** hook to access the current context value: [code]`const { state: { todoList }, dispatch } = React.useContext(TodoContext)`
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
|
|||
|
**TodoProvider.tsx:**
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
type TodoContextProps = {
|
|||
|
state : {todoList: TodoItemProp[]};
|
|||
|
dispatch: ({type, payload}: {type:string, payload: any}) => void;
|
|||
|
}
|
|||
|
|
|||
|
export const TodoContext = React.createContext({} as TodoContextProps);
|
|||
|
|
|||
|
const TodoProvider : React.FC = (props) => {
|
|||
|
const [state, dispatch] = React.useReducer(TodoReducer, {todoList: []});
|
|||
|
const value = {state, dispatch}
|
|||
|
return (
|
|||
|
<TodoContext.Provider value={value}>
|
|||
|
{props.children}
|
|||
|
</TodoContext.Provider>
|
|||
|
)
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
#### Reducers
|
|||
|
|
|||
|
A reducer is a pure function with no side effects. This means that for the same input, the expected output will always be the same. This makes the reducer easier to test in isolation and helps manage state. **TodoReducer** and **FilterReducer** are used in the components **TodoProvider** and **TodoContextApi**.
|
|||
|
|
|||
|
Create a directory named **reducers** under **src** and create a file there named **TodoReducer.tsx**:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
const TodoReducer = (state: StateProps = {todoList:[]}, action: ActionProps) => {
|
|||
|
switch(action.type) {
|
|||
|
case ADD_TODO_ACTION:
|
|||
|
return { todoList: [...state.todoList, action.payload]}
|
|||
|
case REMOVE_TODO_ACTION:
|
|||
|
return { todoList: state.todoList.length ? state.todoList.filter((d) => d.id !== action.payload.id) : []};
|
|||
|
case UPDATE_TODO_ACTION:
|
|||
|
return { todoList: state.todoList.length ? state.todoList.map((d) => {
|
|||
|
if(d.id === action.payload.id) d.completed = !d.completed;
|
|||
|
return d;
|
|||
|
}): []}
|
|||
|
default:
|
|||
|
return state;
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
Create a **FilterReducer** to maintain the filter's state:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
const FilterReducer =(state : TodoItemProp[] = [], action: ActionProps) => {
|
|||
|
switch(action.type) {
|
|||
|
case ALL_FILTER:
|
|||
|
return state;
|
|||
|
case ACTIVE_FILTER:
|
|||
|
return state.filter((d) => !d.completed);
|
|||
|
case COMPLETED_FILTER:
|
|||
|
return state.filter((d) => d.completed);
|
|||
|
default:
|
|||
|
return state;
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
You have created all the required components. Next, you will add the **Header** and **TodoContextApi** components in App, and **TodoContextApi** with **TodoProvider** so that all children can access the context.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
function App() {
|
|||
|
return (
|
|||
|
<div className="App">
|
|||
|
<Header />
|
|||
|
<TodoProvider>
|
|||
|
<TodoContextApi />
|
|||
|
</TodoProvider>
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
Ensure the App component is in **index.tsx** within **ReactDom.render**. [ReactDom.render][15] takes two arguments: React Element and an ID of an HTML element. React Element gets rendered on a web page, and the **id** indicates which HTML element will be replaced by the React Element:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
ReactDOM.render(
|
|||
|
<App />,
|
|||
|
document.getElementById('root')
|
|||
|
);
|
|||
|
```
|
|||
|
|
|||
|
### Conclusion
|
|||
|
|
|||
|
You have learned how to build a functional app in React using hooks and state management. What will you do with it?
|
|||
|
|
|||
|
--------------------------------------------------------------------------------
|
|||
|
|
|||
|
via: https://opensource.com/article/21/3/react-app-hooks
|
|||
|
|
|||
|
作者:[Jaivardhan Kumar][a]
|
|||
|
选题:[lujun9972][b]
|
|||
|
译者:[译者ID](https://github.com/译者ID)
|
|||
|
校对:[校对者ID](https://github.com/校对者ID)
|
|||
|
|
|||
|
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
|||
|
|
|||
|
[a]: https://opensource.com/users/invinciblejai
|
|||
|
[b]: https://github.com/lujun9972
|
|||
|
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/todo_checklist_team_metrics_report.png?itok=oB5uQbzf (Team checklist and to dos)
|
|||
|
[2]: https://reactjs.org/docs/components-and-props.html
|
|||
|
[3]: https://github.com/invincibleJai/todo-app-context-api
|
|||
|
[4]: https://codesandbox.io/s/reverent-edison-v8om5
|
|||
|
[5]: https://opensource.com/sites/default/files/pictures/todocontextapi.gif (React to-do list)
|
|||
|
[6]: https://creativecommons.org/licenses/by-sa/4.0/
|
|||
|
[7]: https://nodejs.org/en/download/
|
|||
|
[8]: https://yarnpkg.com/getting-started/install
|
|||
|
[9]: https://github.com/facebook/create-react-app
|
|||
|
[10]: https://www.typescriptlang.org/
|
|||
|
[11]: https://www.npmjs.com/package/npx
|
|||
|
[12]: https://yarnpkg.com/
|
|||
|
[13]: https://opensource.com/sites/default/files/uploads/to-doapp_architecture.png (To-Do App architecture)
|
|||
|
[14]: https://opensource.com/sites/default/files/uploads/todocomponent_0.png (Todo Component)
|
|||
|
[15]: https://reactjs.org/docs/react-dom.html#render
|