+3

Todo Reactjs và thay đổi Location

Giới thiệu

Reatcjs là một thư viện JavaScript front-end mã nguồn mở và miễn phí để xây dựng giao diện người dùng hoặc các thành phần UI. Nó được duy trì bởi Facebook và một cộng đồng các nhà phát triển và công ty cá nhân. React có thể được sử dụng làm cơ sở để phát triển các ứng dụng một trang hoặc ứng dụng di động. Tuy nhiên, React chỉ quan tâm đến việc quản lý trạng thái và hiển thị trạng thái đó cho DOM , vì vậy việc tạo các ứng dụng React thường yêu cầu sử dụng các thư viện bổ sung để định tuyến, cũng như một số chức năng phía máy khách nhất định.

Quá trình

Trong quá trình học reactjs. Tôi đã học rất nhiều khóa học cơ bản nhưng khi không code một vài tuần tôi lại quên và mơ hồ về các định nghĩa và cách sử dụng, cũng như phân chia components, và các file sao cho nó code logic và hợp lý. Với reactjs là một thư viện, và trong đó có quá nhiều thứ đề lựa chọn. Nó như một bàn ăn và trên đó có cực kỳ nhiều món ăn, nào là axios, react-router-dom, redux-toolkit, react-hook-form, ant design, material ui,... Khi tôi mới vào học kiểu tôi bị ngợp với đống món ăn đây. Nhưng sau quá trinh tìm hiểu và được chỉ bảo thì mình đã hiểu khá rõ về nó. Lời khuyên của mình cho các bạn là nên học javascript nâng cao trước, nói nâng cao nhưng cũng không phải nâng cao mà là nắm chắc về cơ bản và học về es6 và nhiều kiến thức javascript nữa. Qua đống mình cũng nên học và biết chút ít về backend để phục vụ cho việc học về fetch api.

Bắt đầu với todolist

Demo

Bắt đầu với reactjs app

npx create-react-app learn-reactjs
cd learn-reactjs
npm install --save react-hook-form
npm install --save react-router-dom
npm install --save  @material-ui/core
npm install -g sass
npm start

Trong app reactjs mình sẽ tạo các thư mục sau đây.

learn-reactjs
├─ build
├─ node_modules
├─ public
└─ src
  ├─ components
  │  ├─ form-control
  │  │  └─ InputField
  │  │     └─ index.jsx
  │  └─ NotFound
  │     └─ index.jsx
  ├─ features 
  │   └─ Todo
  │     ├─ components
  │     │  ├─ TodoForm
  │     │  │  └─ index.jsx
  │     │  └─ TodoList 
  │     │     ├─ index.jsx
  │     │     └─ style.scss
  │     └─ pages
  │        │  └─ ListPage
  │        │     └─ index.jsx
  │        └─ index.jsx
  ├─ App.css
  ├─ App.js
  └─ index.js

Chỉnh sửa file

index.js

Chỉnh sửa fille index.js trong learn-reactjs

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
      <BrowserRouter>
          <App />
      </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

// 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-vitals
reportWebVitals();

app.js

Chỉnh sửa fille app.js trong learn-reactjs

import { Redirect, Route, Switch } from 'react-router-dom';
import './App.css';
import NotFound from './components/NotFound';
import TodoFeature from './features/Todo';

function App() {

  return (
    <div className="app">
      <Switch>
        <Redirect from="home" to="/" exact />
        <Route path="/todos" component={TodoFeature} />
        <Route component={NotFound} />
      </Switch>
    </div>
  );
}

export default App;

chỉnh sửa app.css trong file learn-reactjs

html{
    background-color: rgba(244,244,244);
}

NotFound

Chỉnh sửa file index.jsx trong notfound

import React from 'react';

NotFound.propTypes = {
};

function NotFound(props) {
    return (
        <div>
            Not Found
        </div>
    );
}

export default NotFound;

form-control

Chỉnh sửa file index.jsx trong InputField. Ở đây chúng ta sử dung Controler để quản lý việc submit form. Khi form được submit thì dữ liệu được control của Controler quản lý sẽ gửi lên thằng cha truyền cho thằng con InputField. Đồng thời ở đây ta có formState lấy errors ra. Nếu có lỗi thì xác định lỗi và hiển thị cho người dùng.

import { TextField } from '@material-ui/core';
import PropTypes from 'prop-types';
import React from 'react';
import { Controller } from "react-hook-form";
import { FormHelperText } from '@material-ui/core';

InputField.propTypes = {
    form: PropTypes.object.isRequired,
    name: PropTypes.string.isRequired,
    label: PropTypes.string,
    disabled: PropTypes.bool,
};

function InputField(props) {

    const { form, name, label, disabled } = props
    const { formState: { errors } } = form
    const hasError = errors[name]

    return (
        <div>
            <Controller
                control={form.control}
                name={name}
                render={({ field }) => (
                    <TextField
                        {...field}
                        fullWidth
                        margin="normal"
                        variant="outlined"
                        label={label}
                        disabled={disabled}
                        error={!!hasError}
                    />
                )}
            />
            <FormHelperText error={!!hasError}>
                {errors[name]?.message}
            </FormHelperText>
        </div>
    );
}

export default InputField;

todo

Chỉnh sửa file index.jsx trong todo. Ở đây chúng ta có sử dụng material-ui bạn có thể tham khảo tại đây để biết các sử dụng. Chúng ta sử dụng useRouteMatch() để lấy đường dẫn hiện tại.

import { makeStyles } from '@material-ui/core';
import React from 'react';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import NotFound from '../../components/NotFound';
import DetailPage from './pages/DetailPage';
import ListPage from './pages/ListPage';

const useStyles = makeStyles((theme) => ({
    root: {
        textAlign: "center",
        padding: "0 0 150px 0",
    },
    header: {
        fontWeight: "200",
        fontSize: "100px",
        color: "#3f50b5",
    },
}))

function TodoFeature(props) {
    const classes = useStyles();
    const match = useRouteMatch();

    return (
        <div className={classes.root}>
            <h1 className={classes.header}>Todo share UI</h1>
            <Switch>
                <Route path={match.path} component={ListPage} exact/>
                <Route component={NotFound} />
            </Switch>
        </div>
    );
}

export default TodoFeature;

todo-form

Chỉnh sửa file index.jsx trong todo-form. Tại đây chúng ta cũng chỉnh sửa giao diện bằng material-ui. Ở đây chúng ta tạo ra useForm để lấy dữ liệu và schema để xác định lỗi cho form. Bạn có thể tìm hiểu ở react-hook-form. chúng ta có handleSubmit() để nhận dữ liệu từ form và xử lý khi form.handleSubmit được gọi.

import React from 'react';
import PropTypes from 'prop-types';
import InputField from '../../../../components/form-control/InputField';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";
import { makeStyles } from '@material-ui/core';

const useStyles = makeStyles((theme) => ({
    root: {
        width: "40%",
        margin: "0 auto",
    },
}))

TodoForm.propTypes = {
    onSubmit: PropTypes.func,
};

function TodoForm(props) {

    const classes = useStyles()

    const { onSubmit } = props

    const schema = yup.object().shape({
        title: yup.string().required("Please enter title")
            .min(3, "Title is too short"),
    });

    const form = useForm({
        defaultValues: {
            title: '',
        },
        resolver: yupResolver(schema),
    })

    const handleSubmit = (values) => {
        if (onSubmit) {
            onSubmit(values)
        }
        form.reset()
    }

    return (
        <div className={classes.root}>
            <form onSubmit={form.handleSubmit(handleSubmit)}>
                <InputField name="title" label="Todo" form={form} />
            </form>
        </div>
    );
}

export default TodoForm;

todo-list

Chỉnh sửa file index.jsx trong todo-list. Tại đây chúng ta nhận từ thằng cha 2 tham số đó là todoList(list), onTodoClick(function). Khi item nào trong danh sách được click thì chúng ta gọi handlerTodoClick và hàm này sẽ gọi onTodoClick và truyền dữ liệu từ thằng cha truyền vào để thằng cha xử lý.

import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import './styles.scss';
import { makeStyles } from '@material-ui/core';

const useStyles = makeStyles((theme) => ({
    root: {

    },
}))

TodoList.propTypes = {
    todosList: PropTypes.array,
    onTodoClick: PropTypes.func,
};

TodoList.defaultProps = {
    todosList: [],
    onTodoClick: null,
}

function TodoList(props) {
    const classes = useStyles();

    const { todoList, onTodoClick } = props;

    const handlerTodoClick = (todo, id) => {
        if(!onTodoClick) return ;
        onTodoClick(todo, id);
    }

    return (
        <div className={classes.root}>
            <ul className="todo-list">
                {todoList.map((todo,index) => (
                    <li className={classnames({ 
                        'todo-item':true,
                        completed: todo.status === 'completed' 
                    })} 
                    key={todo.id}
                    onClick={() => handlerTodoClick(todo, todo.id)}
                    >
                        {todo.title}
                    </li>
                ))}
            </ul>
        </div>
    );
}

export default TodoList;

ListPage

Chỉnh sửa file index.jsx trong ListPage.Ở đây chúng ta tạo ra state todoList để lưu state với useState . Tại đấy chúng ta viết hàm handlerTodoClick để xử lý dữ liệu từ thằng con gửi lên. Chúng ta cũng tạo một state filterStatus để tìm kiếm các state của todo(ví dụ: Hoàn thành, chưa hoàn thành, ...) đồng thời sử dụng history để cập nhật đượng dẫn ở location. Chúng tat sử dụng useLocation() để lấy location hiện tại. và useRouteMatch() để lấy pathname hiện tại để phục vụ cho nhu cầu update location.

import { Button, makeStyles } from '@material-ui/core';
import queryString from 'query-string';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import TodoForm from '../../components/TodoForm';
import TodoList from '../../components/TodoList';

const useStyles = makeStyles((theme) => ({ 
    button: {
        margin: theme.spacing(4, 2),
    },
}))

function ListPage(props) {

    const initTodoList = [
        {
            id: 1,
            title: 'Eat',
            status: 'new',
        },
        {
            id: 2,
            title: 'Sleep',
            status: 'completed',
        },
        {
            id: 3,
            title: 'Code',
            status: 'new',
        },
        {
            id: 4,
            title: 'Play Game',
            status: 'completed',
        },
    ]

    const classes = useStyles();

    const location = useLocation(); //lấy location hiện tại. location.search = ?status=
    const history = useHistory(); //lấy history để thay đổi location hiện tại. 
    const match = useRouteMatch(); //sử dụng mathc để lấy path name hiện tại
    const [todoList, setTodoList] = useState(initTodoList);
    const [filterStatus, setFilterStatus] = useState(() => {
        const params = queryString.parse(location.search);
        return params.status || 'all';
    });

    useEffect(() => {
        const params = queryString.parse(location.search);
        setFilterStatus(params.status || 'all')
    }, [location.search]) //khi location hiện tại hay đổi thì mình sẽ thực hiện useEffect

    const handlerTodoClick = (todo, id) => {
        //clone current array to the new array
        const newTodoList = [...todoList]
        //toggle state
        let toggleTodo = null;
        let index = 0;
        for (let i = 0; i < newTodoList.length; i++) {
            if (newTodoList[i].id === id) {
                toggleTodo = newTodoList[i];
                index = i;
                break;
            }
        }
        const newTodo = {
            ...newTodoList[index],
            status: toggleTodo.status === 'new' ? 'completed' : 'new',
        }
        newTodoList[index] = newTodo
        //update todo list
        setTodoList(newTodoList)
    }

    const handleShowAllClick = () => {
        const queryParams = { status: 'all' }
        history.push({
            pathname: match.path,
            search: queryString.stringify(queryParams),
        })
    }

    const handleShowCompletedClick = () => {
        const queryParams = { status: 'completed' }
        history.push({
            pathname: match.path,
            search: queryString.stringify(queryParams),
        })
    }
    const handleShowNewClick = () => {
        const queryParams = { status: 'new' }
        history.push({
            pathname: match.path,
            search: queryString.stringify(queryParams),
        })
    }

    const renderTodoList = useMemo(() => {
        return todoList.filter(todo => filterStatus === 'all' || filterStatus === todo.status);
    }, [todoList, filterStatus])

    const handleTodoFormSubmit = (values) => {
        console.log(values)
        const newTodo = {
            id: todoList.length+1,
            title: values.title,
            status: 'new'
        }

        const newTodoList = [...todoList, newTodo]
        setTodoList(newTodoList)
    }

    return (
        <div>
            <TodoForm onSubmit={handleTodoFormSubmit}/>
            <TodoList todoList={renderTodoList} onTodoClick={handlerTodoClick} />
            <Button className={classes.button} color="primary"  variant="contained" onClick={handleShowAllClick}>Show All</Button>
            <Button className={classes.button} color="primary"  variant="contained" onClick={handleShowCompletedClick}>Show Completed</Button>
            <Button className={classes.button} color="primary"  variant="contained" onClick={handleShowNewClick}>Show New</Button>
        </div>
    );
}

export default ListPage;

Đến đây là đã hoàn thành dự án TodoList rồi. Chúc các bạn thành công


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.