拒绝代码冗余:React 自定义组件的封装实战

96 阅读3分钟

之前写代码总是把逻辑和 UI 混在一起,写出来的组件又长又臭,自己都不想看。今天我尝试了"自定义 Hook",感觉像打开了新世界的大门!

如果你也像我一样刚开始学 React,希望这篇笔记能帮到你。

1. 从一个简单的鼠标追踪开始

最开始,我想做一个简单的功能:在屏幕上显示鼠标的坐标。 按照以前的习惯,直接在组件里写了 useStateuseEffect

这是我一开始写在组件里的代码(虽然能跑,但是逻辑都堆在组件里):

function MouseMove() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);
  
  useEffect(() => {
    const updata = (event) => {
      console.log('/////////////////////');
      setX(event.pageX);
      setY(event.pageY);
    }
    window.addEventListener('mousemove',updata);
    console.log('组件挂载时执行');
    return () => {
      console.log('组件卸载时执行');
      window.removeEventListener('mousemove',updata); // 组件卸载时,消除事件监听,没有则内存泄漏
    }
  }, [])
  
  return (
    <>
      <p>鼠标位置:{x}, {y}</p>
    </>
  )
}

2. 第一次尝试封装:useMouse

React 的核心思想之一就是"复用"。这种监听鼠标位置的逻辑,如果别的组件也要用,复制粘贴可行但在实际开发中不可取!

于是,我尝试把它抽离成一个自定义 Hook。文件放在 src/hooks/useMouse.js

import { useState , useEffect } from 'react';

// 封装响应式mouse业务
// UI组件更简单 HTML+CSS,好维护
// 复用
export const useMouse = () => {
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);
    useEffect(() => {
      const updata = (event) => {
        console.log('/////////////////////');
        setX(event.pageX);
        setY(event.pageY);
      }
      window.addEventListener('mousemove',updata);
      console.log('组件挂载时执行');
      return () => {
        console.log('组件卸载时执行');
        window.removeEventListener('mousemove',updata); // 组件卸载时,消除事件监听,没有则内存泄漏
      }
    }, [])
    // 把要向外部暴露的状态和方法返回
    return {
        x,
        y
    }
}

现在组件瞬间变干净了

// 现在的 MouseMove 组件
function MouseMove() {
  // 引入自定义hook,一行代码搞定逻辑!
  const {x,y} = useMouse();

  return (
    <>
      <p>鼠标位置:{x}, {y}</p>
    </>
  )
}

image.png

3. 进阶挑战:做一个 TodoList

只有鼠标追踪太简单了,拿最经典的"待办事项列表"来练手。这次我决定彻底贯彻"UI 和逻辑分离"的思想。

第一步:先把 UI 组件写好

1. 输入框组件 (src/components/TodoInput.jsx) 负责接收用户输入,用户按回车或者提交时,把数据传出去。

import { useState } from 'react';

export default function TodoInput({onAddTodo}) {
    const [text, setText] = useState('');
    const handleSubmit = (e) => {
        e.preventDefault();
        if (!text.trim()) {
            return;
        }
        onAddTodo(text.trim());
        setText('');
    }
    return (
        <form className ="todo-input" onSubmit={handleSubmit} >
            <input 
                type="text" 
                value={text}
                onChange={e => setText(e.target.value)}
            />
        </form>
    )
}

2. 单个列表项组件 (src/components/TodoItem.jsx) 负责显示一条具体的任务,可以勾选完成,也可以删除。

export default function TodoItem({todo,onToggle,onDelete}) {
    // console.log(todo,'//////');
    return (
        <li className='todo-item'>
            <input
                type='checkbox'
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
            />
            <span className={todo.completed ? 'completed' : ''}>
                {todo.text}
            </span>
            <button onClick={() => onDelete(todo.id)}>删除</button>
        </li>
    )
}

3. 列表容器组件 (src/components/TodoList.jsx) 它只负责遍历数据,渲染一个个 TodoItem

import TodoItem from './TodoItem.jsx';

export default function TodoList({todos,onToggle,onDelete}) {
    // console.log(todos,'//////');
    return (
        <ul className='todo-list'>
            {
                todos.map(todo => (
                    <TodoItem
                        key={todo.id}
                        todo={todo}
                        onToggle={onToggle}
                        onDelete={onDelete}
                    />
                ))
            }
        </ul>

    )
}

第二步:编写大脑 useTodos

UI 写好了,现在最重要的问题来了:数据存在哪?增删改查的逻辑写在哪? 如果写在 App.jsx 里,那个文件很快就会爆掉。所以,要把这些逻辑封装进 src/hooks/useTodos.js

这里还加了个功能:把数据自动保存到 localStorage,这样刷新页面数据还在!

// 封装响应式todos业务
import { useState , useEffect } from 'react';

const STORAGE_KEY = 'todos'; // 好维护

function loadFromStorage() {
    const todos = localStorage.getItem(STORAGE_KEY);
    return todos ? JSON.parse(todos) : [];
}

// 保存todos到localStorage
function saveToStorage(todos) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

export const useTodos = () => {
    // useState 接收函数 计算 同步
    const [todos, setTodos] = useState(loadFromStorage);

    // 监听todos变化 保存到localStorage
    useEffect(() => {
        saveToStorage(todos);
    }, [todos]);

    // 添加todo
    const addTodo = (text) => {
        setTodos([...todos, {id: Date.now(), text, completed: false}]);
    }

    // 切换todo完成状态
    const toggleTodo = (id) => {
        setTodos(
        todos.map(todo => {
            if(todo.id === id) {
            return {
                ...todo,
                completed: !todo.completed
            }
            }
            return todo;
        })
        )
    }

    // 删除todo
    const deleteTodo = (id) => {
        setTodos(todos.filter(todo => todo.id !== id));
    }

    return {
        todos,
        addTodo,
        toggleTodo,
        deleteTodo,
  }
}

4. 最后的组装:App.jsx

最后,在 src/App.jsx 里,我只需要像搭积木一样,把 UI 组件摆好,然后用 useTodos 把逻辑"注入"进去。 看看现在的 App.jsx 有多清爽!

import { useState , useEffect } from 'react';
import { useMouse } from './hooks/useMouse.js';
import { useTodos } from './hooks/useTodos.js';
import TodoList from './components/TodoList.jsx';
import TodoInput from './components/TodoInput.jsx';


function MouseMove() {
   // 省略上面演示过的代码...
   // 引入自定义hook
  const {x,y} = useMouse();

  return (
    <>
      <p>鼠标位置:{x}, {y}</p>
    </>
  )
}

export default function App() {
  const [count, setCount] = useState(0);
  // 这里直接使用我们封装好的 hook
  const {todos, addTodo,toggleTodo,deleteTodo} = useTodos();

  return (
    <>
      <TodoInput onAddTodo={addTodo} />
      {
        todos.length > 0 ?( <TodoList 
          todos={todos} 
          onToggle={toggleTodo} 
          onDelete={deleteTodo} />) : (
          <div>暂无待办事项</div>
        )
      }
      {/* 
      <button onClick={() => setCount((count) => count + 1)}>
        点击增加
      </button>
      {count % 2 === 0 && <MouseMove />} */}
    </>
  )
}

image.png