xianzou / blog

弦奏的博客 一个混迹多年的前端开发人员,正在努力的学习中
17 stars 2 forks source link

关于React Hook代码逻辑分层思考 #48

Open xianzou opened 2 years ago

xianzou commented 2 years ago

状态的分层设计

最底层的内置hook,不需要自己实现,官方直接提供

state分层

简化状态更新方式的hook,更方便地进行不可变更新的目的

ahookuseSetState

管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致;

引入“状态 + 行为”的概念,通过声明状态结构与相应行为快速创建一个完整上下文

针对通用业务场景进行封装,如分页的列表、滚动加载的列表、多选等。

实际面向业务的实现。

react hook 逻辑分层

背景或烦恼:

​ 我们前端在开发一个业务的时候,总是先从界面出发,看着界面想我这里要怎么做怎么做,等把界面交互大致写出来之后,再把产品文档里面的业务逻辑作为一些判断条件加入到写好的交互代码中,最终交付;

​ 随着业务持续增长,代码越撑越大,当到一定程度之后,根本不敢改一行代码,因为怕一改就影响整个业务;

为什么会这样

​ 代码同时承载了业务的逻辑和界面的交互逻辑,业务逻辑和交互逻辑耦合,因为这种线性的开发思维,让我们写的组件随着业务的扩展,越来越难以高效的维护,直到最后不敢修改一行。

解决方案

​ 类似耳机线,无论耳机线材质有多坚韧,直接塞进裤子里一定会打结,怎么避免,就需要一个耳机线盒来规整线圈,有了盒子的约束和隔离,打结的概率就变得很低;同理,我们也需要对代码进行一定的管理,重新梳理;

理念:

​ 坚持无状态的有渲染,有状态的无渲染;

前端代码分层

底层/第三方hook

react hookahook等

状态与操作封装

意图:把一个状态和它强相关的行为放在一起

给我一个值和一堆方法,我帮你变成hook

const useMethods = (initialValue, methods) => {
    const [value, setValue] = useState(initialValue);
    const boundMethods = useMemo(
        () => Object.entries(methods).reduce(
            (methods, [name, fn]) => {
                const method = (...args) => {
                    setValue(value => fn(value, ...args));
                };
                methods[name] = method;
                return methods;
            },
            {}
        ),
        [methods]
    );
    return [value, boundMethods];
};

封装基于数据结构或一些特定场景的操作

useArray 示例

const arrayMethods = {
    push(state, item) {
        return state.concat(item);
    },
    pop(state) {
        return state.slice(0, -1);
    },
    slice(state, start, end) {
        return state.slice(start, end);
    },
    clear() {
        return [];
    },
    set(state, newValue) {
        return newValue;
    },
    removeIndex(index) {
      setValue(v => {
        const copy = v.slice();
        copy.splice(index, 1);
        return copy;
      });
    },
};
//操作数组的hook
const useArray = (initialValue = []) => {
    return useMethods(initialValue, arrayMethods);
};
# 使用 const [arr,{push,removeIndex}] = useArray([]);

//操作数字类型的hook
const useNumber = (initialValue = 0)=>{
    return useMethods(initialValue, number对应的方法);
}

基于 useContext更方便实现跨组件共享 state 的管理

import React, { createContext, useContext } from "react";

const createContainer = (useHook) => {
  const Context = createContext();

  const useContainer = () => {
    return useContext(Context);
  };

  const Provider = ({ initialState, children }) => {
    const value = useHook(initialState);
    return <Context.Provider value={value}>{children}</Context.Provider>;
  };

  return { Provider, useContainer }
};

# 使用
const useCounter = () => {
  let [count, setCount] = useState(0)
  let add = () => setCount(count + 1)
  return { count, add }
}

const Counter = createContainer(useCounter);

const APP = () => {
  return (
    <Counter.Provider>
      <CounterDisplay />
    </Counter.Provider>
  )
}
# 内部可以拿到 useCounter 返回的 count、add

其他 参考 快速上手 - ahooks 3.0

通用过程封装

调用请求,并同步到状态里,类似给我一个异步函数,我帮你调用它并管理异步状态;

const useTaskPending = task => {
    const [pendingCount, {increment, decrement}] = useNumber(0);
    const taskWithPending = useCallback(
        async (...args) => {
            increment();
            const result = await task(...args);
            decrement();
            return result;
        },
        [task, increment, decrement]
    );
    return [taskWithPending, pendingCount > 0];
};

const useTaskPendingState = (task, storeResult) => {
    const [taskWithPending, pending] = useTaskPending(task);
    const callAndStore = useCallback(
        () => {
            const result = await taskWithPending();
            storeResult(result);
        },
        [taskWithPending, storeResult]
    );
    return [callAndStore, pending];
};

公共可复用的模块逻辑

弹窗useModal,useToggle,useList,useForm,useTree,useScoll

业务层的业务逻辑

保存提交,页面初始化话请求等和视图无关

视图层的交互逻辑

按钮点击事件、跳转,初始化、弹窗

最后,最顶层组织各业务模块;

改造示例

业务hook,提供加载,添加,删除

const useUserList = () => {
    const [pending, setPending] = useState(false);
    const [users, setUsers] = useState([]);
    const load = async params => {
        setPending(true);
        setUsers([]);
        const users = await request('/users', params);
        setUsers(users);
        setPending(false);
    };
    const deleteUser = useCallback((user) =>{
        const result = Dialog.confirm('确认删除吗');
        setUsers(users => without(users, user));
    }, []);
    const addUser = useCallback(
        user => setUsers(users => users.concat(user)),
        []
    );
    return [users, {pending, load, addUser, deleteUser}];
};
# view.js
const View = ()=>{
    const [users,operation] = useUserList();
    return (
        <ul>
            {
               users.map(item =>(<li onclick={operation.addUser}>{item.name}</li>)) 
            }
        </ul>
    )
}

拆分以上功能

  1. 加载一个远程数据,并且控制“加载中”状态。
  2. 往一个数组中增加或删除内容。
  3. 将一份数据(列表)和这份数据的相关操作(add、delete)合在一起返回
  4. 指定加载用户列表这个具体业务场景

通过以上拆解可以发现,1-3全是通用能力,而不是业务相关的,通过以上示例,拼装业务,实际代码如下:

#listUsers为接口
const useUserList = () => {
    const [users, {push, remove, set}] = useArray([]);
    const [load, pending] = useTaskPendingState(listUsers, set);
    return [users, {pending, load, addUser: push, deleteUser: remove}];
};

const useViewHandle = ({push,remove})=>{
    const onAddHandle = ()=>{
        push();
    }
    const onRemoveHandle = ()=>{
        const result = Dialog.confirm('确认删除吗');
        result && remove();
    }
    return {
        onAddHandle,
        onRemoveHandle
    }
}

# view.js
const View = ()=>{
    const [users,operation] = useUserList();
    const {onAddHandle,onRemoveHandle} = useViewHandle(operation);
    return (
        <ul>
            {
               users.map(item =>(<li onClick=>{item.name(onAddHandle)}</li>)) 
            }
        </ul>
    )
}

参考资料

前端分层:把业务逻辑从交互代码中解救出来_唐霜的博客 (tangshuang.net)

React Hooks 实践指南 · Issue #13 · rainjay/blog (github.com)

(64 封私信 / 81 条消息) 如何去合理使用 React hook? - 知乎 (zhihu.com)