Skip to content

一个案例通关 React 核心知识点

搭建项目

js
npm create vite@latest

image.png

下载依赖,然后启动

image.png

编写应用基本结构

将 scr/App.jsx 改为一下代码:

js
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'

function App() {

  return (
    <div className='bg'>
      <h2>
        我的待办事项<img src={reactLogo}></img>
      </h2>
      <ul>
        <li className='item'>学习 vue</li>
        <li className='item'>学习 react</li>
      </ul>
    </div>
  )
}

export default App

循环输出项目

用一个数组表示所有 todo 项

js
  const todoList = ['vue', 'react', '后台管理系统', '组件源码']

,然后渲染到页面上。

js
<>
  <h2>
    我的待办事项<img src={reactLogo}></img>
  </h2>
  <ul>
    {
      todoList.map(item => <li className='item' key={item}>{item}</li>)
    }
  </ul>
</>

为 todo 加入是否完成字段

todo 是否完成需要有一个字段来判断,所以改下 todoList 的结构

js
  const todoList = [
    { title: 'vue', completed: true, id: 1 },
    { title: 'react', completed: false, id: 2 },
    { title: '后台管理系统', completed: false, id: 3 },
    { title: '组件源码', completed: false, id: 4 },
  ]

顺便改下视图结构。

js
    <>
      <h2>
        我的待办事项<img src={reactLogo}></img>
      </h2>
      <ul>
        {
          todoList.map(item => <li className='item' key={item.id}>{item.title}</li>)
        }
      </ul>
    </>

用 checkbox 来显示 todo 是否完成

用户的 todo 是否完成可以用 checkbox 来显示。

为了让我们改变 todo 时视图也会跟着变化,这里用 react 自带的 hook useState 将 todoList 包裹。

这里村长也提到了受控组件的概念:react 的 state 作为表单的唯一数据源,同时表单触发的一系列事件也由 react 处理。

js
  const [todoList, setTodoList] = useState([
    { title: 'vue', completed: true, id: 1 },
    { title: 'react', completed: false, id: 2 },
    { title: '后台管理系统', completed: false, id: 3 },
    { title: '组件源码', completed: false, id: 4 },
  ])

  function changeState(e, item) {
    // 因为是引用数据类型,所以 todoList 也会跟着改变
    item.completed = e.target.checked

    // 想要让视图更新,必须 setTodoList 一下

    // 这样是不会触发视图更新的,因为 react 比较的时候会发现和原来的值相同就不去更新了。
    // setTodoList(todoList)
    
    setTodoList([...todoList])
  }
js
    <>
      <h2>
        我的待办事项<img src={reactLogo}></img>
      </h2>
      <ul>
        {
          todoList.map(item => {
            return (
              <li className='item' key={item.id}>
                <input type='checkbox' checked={item.completed} onChange={(e) => changeState(e, item)}/>
                <span>{item.title}</span>
              </li>
            )
          })
        }
      </ul>
    </>

效果如下

image.png

新增待办事项

这同样是受控组件的应用。

添加一个输入框:

js
      <input
        className="new-todo"
        autoFocus
        autoComplete="off"
        placeholder="该学啥了?"
        value={newTodo}
        onChange={changeNewTodo}
        onKeyUp={addTodo}
      />

对应的状态和事件控制:

js
  const [newTodo, setNewTodo] = useState("")

  function changeNewTodo(e) {
    setNewTodo(e.target.value);
  }

  // 用户回车且输入框有内容则添加一个新待办
  function addTodo(e) {
    if (e.code === 'Enter' && newTodo) {
      setTodoList([
        ...todoList,
        {
          id: todoList.length + 1,
          title: newTodo,
          completed: false,
        },
      ]);
      setNewTodo("");
    }
  };

删除待办事项

新增一个删除按钮:

js
<button className='x-button' onClick={() => removeTodo(item)}>X</button>

修改状态,filter 会返回一个新的数组

js
  function removeTodo(item) {
    setTodoList(todoList.filter(todoItem => todoItem.id !== item.id))
  }

修改待办事项

未命名 ‑ Made with FlexClip.gif

想要实现的功能:双击 title,进入编辑模式。按回车如果输入不为空,则修改成功,否则提示标题不能为空。失去焦点退出编辑模式时,会提示确认修改。

首先修改下视图,用一个 editedTodo 表示当前被编辑的 todo 副本。

js
  <li className='item' key={item.id}>
    <input className='checkbox' type='checkbox' checked={item.completed} onChange={(e) => changeState(e, item)}/>
    {
        editedTodo.id !== item.id ? 
        <>
          <span onDoubleClick={(e) => {onDoubleClick(item)}}>{item.title}</span>
          <button className='x-button' onClick={() => removeTodo(item)}>X</button>
        </>
        :
        <>
          <input 
            type='text' 
            value={editedTodo.title} 
            onChange={onEditing}
            onKeyUp={onEditComplete}
            onBlur={onEditBlur}
          />
        </>
    }
  </li>

实现相关 js 逻辑

js
  // 当前正在被编辑的 todo 的副本
  const [editedTodo, setEditedTodo] = useState(inital)

  // 双击进入编辑模式
  function onDoubleClick(item) {
    setEditedTodo({...item})
  }

  // 编辑输入
  function onEditing(e) {
    const title = e.target.value;
    setEditedTodo({ ...editedTodo, title });
  }

  // 按回车编辑完成
  function onEditComplete(e) {
    if(e.code === 'Enter') {
      const title = e.target.value;
      if(!title) {
        alert('title 不能为空!')
      } else {
        noName(title)
      }
      setEditedTodo(inital)
    }
  }

  // 无名函数,作用为修改 todoList
  function noName(title) {
    const todo = todoList.find(item => item.id === editedTodo.id)
    todo.title = title
    setTodoList([...todoList])
  }

  // 编辑时失去焦点
  function onEditBlur(e) {
    const title = e.target.value;
    if(!title) {
      alert('title 不能为空!')
    } else {
      if(confirm('确认修改吗?')) {
        noName(title)
      }
    }
    setEditedTodo(inital)
  }

自动获取焦点

现在进入编辑模式后,还不能自动获取焦点。

虽然我发现只要在编辑模式下的 input 上加一个 autoFocus 属性就可以实现了,不用 ref。

不过在这里还是用 ref 实现下,顺便复习下 ref 和 useEffect 的用法。

js
  <input 
    ref={e => setEditInputRef(e, item)}
    ...
  />

js 逻辑

js
  let inputRef = null
  const setEditInputRef = (e, todo) => {
    if (editedTodo.id === todo.id) {
      inputRef = e
    }
  }

  useEffect(() => {
    if (editedTodo.id) {
      inputRef.focus()
    }
  }, [editedTodo])

状态持久化

存储在本地

js
  const STORAGE_KEY = 'todomvc-react'
  const todoStorage = {
    fetch () {
      // get 到了一个新写法,以前我都是用三元运算符写的,很麻烦
      const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
      return todos
    },
    save (todos) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
    }
  }

  const [todoList, setTodoList] = useState(todoStorage.fetch())

  useEffect(() => {
    todoStorage.save(todoList)
  }, [todoList])

提取组件

如果是 vue 的话,我一般不习惯把这种列表封装成组件,但是经常把列表项封装为一个组件。像下面这样:

js
<ListItem {...{isEditing: editedTodo.id === item.id, editedTodo}}></ListItem>

自定义 hooks

自定义组件,一般是现在父组件中写好,再提取为自定义组件。

自定义 hooks,则是先自定义一个 hook,再引入使用。(个人观点)

首先自定义一个 hook,useTodoList.jsx

js
import { useState } from "react";

// 接收初始数据,将其声明为状态,同时提供状态操作方法给外界使用

// 我感觉有点先定义了一个 state,然后把操作 state 的增删改查方法都定义好了,再都抛出去。
// 复用的都是一些操作 state 的方法
export function useTodoList(data) {
  const [todoList, setTodoList] = useState(data)

  function addTodo(title) {
    setTodoList([
      ...todoList,
      {
        id: todoList.length + 1,
        title,
        completed: false,
      }
    ])
  }

  function removeTodo(id) {
    setTodoList(todoList.filter(item => item.id !== id))
  }

  function updateTodo(editedTodo) {
    const todo = todoList.find(item => item.id === editedTodo.id)
    Object.assign(todo, editedTodo)
    setTodoList([...todoList])
  }

  return {todoList, addTodo, removeTodo, updateTodo, setTodoList}

}

然后再 App.jsx 中结构使用:

js
const {todoList, addTodo, removeTodo, updateTodo, setTodoList} = useTodoList(todoStorage.fetch())

App.jsx 中也有一些变化

js
  const [newTodo, setNewTodo] = useState("")
  function changeNewTodo(e) {
    setNewTodo(e.target.value);
  }

  // 用户回车且输入框有内容则添加一个新待办
  function onAddTodo(e) {
    if (e.code === 'Enter' && newTodo) {
      addTodo(newTodo)
      setNewTodo("");
    }
  };
  
  
    // 按回车编辑完成
  function onEditComplete(e) {
    if(e.code === 'Enter') {
      const title = e.target.value;
      if(!title) {
        alert('title 不能为空!')
      } else {
        // noName(title)
        updateTodo(editedTodo)
      }
      setEditedTodo(inital)
    }
  }

  // 编辑时失去焦点
  function onEditBlur(e) {
    const title = e.target.value;
    if(!title) {
      alert('title 不能为空!')
    } else {
      if(confirm('确认修改吗?')) {
        // noName(title)
        updateTodo(editedTodo)
      }
    }
    setEditedTodo(inital)
  }

过滤功能

最后一个过滤功能,

也是使用到了自定义 hook,还有 react 内置钩子 useMemo。useMemo 字面意思就是使用缓存,也就是说只有当传入依赖项改变时,才会重新执行传入的回调参数。

useFilter.jsx

js
function useFilter(todos) {  
  const [visibilitysetVisibility= useState("all");  
  // 如果 todos 或者 `visibility` 变化,我们将重新计算 `filteredTodos`  
  const filteredTodos = useMemo(() => {  
    if (visibility === "all") {  
      return todos;  
    } else if (visibility === "active") {  
      return todos.filter((todo=> todo.completed === false);  
    } else {  
      return todos.filter((todo=> todo.completed === true);  
    }  
  }, [todos, visibility]);  
  return {visibility, setVisibility, filteredTodos}  
}

使用:

js
const {visibilitysetVisibilityfilteredTodos= useFilter(todos)
js
<TodoFilter visibility={visibility} setVisibility={setVisibility}></TodoFilter>

最后感想

跟着村长的教程大概敲了一般,感觉对 react hook 的使用似乎有那么一点点理解了。嘻嘻。

原文链接:https://mp.weixin.qq.com/s/2x884JDNZvlAPfa0Eq9uMw

Released under the MIT License.