React 最佳实践

268 阅读6分钟

使用React写业务也有挺长一段时间了,总结下我认为的最佳实践。

状态管理最佳实践

主流的状态管理多多少少都用过,多少都有学习成本。 真的有必要花大把时间去学其他的状态管理吗?真的实用吗?如果不是为了应付面试,我是不会花时间去了解那些状态管理的。

后面发现 useContext hook 完全够用,而且对逻辑的划分非常有帮助,真正做到了逻辑的复用。 如果你习惯用自定义 hook 去抽离逻辑,那么结合 useContext 会是个很棒的体验。

这是对 useContext 很棒的封装: github.com/jamiebuilds…

image.png

举例:

import React, { useState } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"

// 定义 A 相关的状态和逻辑
const useA = () => {
    const [a, setA] = useState(0)
    const handleChangeA = () => {
        setA(a + 1)
    }
    return {
        a,
        handleChangeA
    }
}
const AContainer = createContainer(useA)

// 定义 subType 相关的状态和逻辑
const subTypeMap = {
    0: 'all',
    1: 'active',
    2: 'completed'
}
const useSubTypeSelect = () => {
    const [subType, setSubType] = useState(0);
    const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
    // ReactNode
    const subTypeNode = <div>
        <h5>subType: {subTypeMap[subType]} </h5>
        <ul>
            {subTypeOptions.map((item) => (
                <li
                    key={item.value}
                    onClick={() => setSubType(item.value)}
                >
                    {item.label}
                </li>
            ))}
        </ul>
    </div>
    // ... others
    return {
        subType,
        subTypeNode
    }
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)

// UI
const Main = () => {
    // 使用 A 模块相关状态和逻辑
    const { a, handleChangeA } = AContainer.useContainer()
    // 使用 subType 模块相关状态和逻辑
    const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
    return <div>
        {/* A */}
        <button onClick={handleChangeA}>A: {a}</button>
        {/* subTypeSelect */}
        {subTypeNode}
        {/* ... */}
    </div>
}


const App = () => {
    return (
        <AContainer.Provider>
            <SubTypeSelectContainer.Provider>
                <Main />
            </SubTypeSelectContainer.Provider>
        </AContainer.Provider>
    )
}

render(<App />, document.getElementById("root"))

效果:

20240418175652_rec_.gif

state 与 queryString 联动

有些时候,你希望你的state在变更时同时变更URL里的queryString,那么可以这么做:

  1. 定义 useQSState
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';

export const encodeParams: <T> (obj: T) => string = obj => encodeURIComponent(JSON.stringify(obj));
// export const encodeParamsWithOptionsProperty: <T>  (obj: T) => string = obj => encodeParams({ options: obj });
export const encodeParamsSplicedOptions: <T>(obj: T) => string = obj => `?options=${encodeParams(obj)}`;
export const deCodeParams = (qs: string) => {
    try {
        return JSON.parse(decodeURIComponent(qs));
    } catch (error) {
        return {}
    }
};

const getQSObjectWithSplitedOptions: () => Record<string, unknown> = () => {
    const _qs = window.location.href?.split('?')?.[1];
    return deCodeParams(_qs?.split('=')?.[1])
};
export const useQueryString = () => {
    const queryStringObj: Record<string, any> = getQSObjectWithSplitedOptions();
    const history = useHistory();
    const setQueryString = rawQueryString => {
        if (history.location.search !== rawQueryString) {
            history.replace({
                search: rawQueryString,
            });
        }
    };
    const setQSByObj = (newObject): void => {
        const rawQueryString = encodeParamsSplicedOptions(newObject);
        setQueryString(rawQueryString);
    };

    const injectQS = injectedObj => {
        const _qs = getQSObjectWithSplitedOptions();

        const newObject = {
            ..._qs,
            ...injectedObj,
        };
        setQSByObj(newObject);
    };
    const injectQSByProperty = (propName: string, propValue) => {
        injectQS({ [propName]: propValue });
    };

    return { queryStringObj, setQueryString, setQSByObj, injectQS, injectQSByProperty, };
};
// state 与 queryString 联动
export const useQSState: <T>(field: string, defaultValue: T) => [T, React.Dispatch<React.SetStateAction<T>>] = (field, defaultValue) => {
    const { queryStringObj, injectQSByProperty } = useQueryString();
    const _defaultValue = queryStringObj[field] || defaultValue;
    const [value, setValue] = useState(_defaultValue);
    useUpdateEffect(() => {
        injectQSByProperty(field, value);
    }, [value]);
    return [value, setValue];
}

  1. 使用 useQSState
import React, { useState } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
import { useQSState } from './useQueryString'

// 定义 A 相关的状态和逻辑
const useA = () => {
    const [a, setA] = useQSState('a', 0) // useState 被替换成 useQSState
    const handleChangeA = () => {
        setA(a + 1)
    }
    return {
        a,
        handleChangeA
    }
}
const AContainer = createContainer(useA)

// 定义 subType 相关的状态和逻辑
const subTypeMap = {
    0: 'all',
    1: 'active',
    2: 'completed'
}
const useSubTypeSelect = () => {
    const [subType, setSubType] = useQSState('subType', 0); // useState 被替换成 useQSState
    const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
    // ReactNode
    const subTypeNode = <div>
        <h5>subType: {subTypeMap[subType]} </h5>
        <ul>
            {subTypeOptions.map((item) => (
                <li
                    key={item.value}
                    onClick={() => setSubType(item.value)}
                >
                    {item.label}
                </li>
            ))}
        </ul>
    </div>
    // ... others
    return {
        subType,
        subTypeNode
    }
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)

// UI
const Main = () => {
    // 使用 A 模块相关状态和逻辑
    const { a, handleChangeA } = AContainer.useContainer()
    // 使用 subType 模块相关状态和逻辑
    const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
    return <div>
        {/* A */}
        <button onClick={handleChangeA}>A: {a}</button>
        {/* subTypeSelect */}
        {subTypeNode}
        {/* ... */}
    </div>
}


const App = () => {
    return (
        <AContainer.Provider>
            <SubTypeSelectContainer.Provider>
                <Main />
            </SubTypeSelectContainer.Provider>
        </AContainer.Provider>
    )
}

render(<App />, document.getElementById("root"))

state 之间的联动

有些时候,改变某个state,其他state会受到影响 比如列表页,当我们变更筛选项或者排序时,需要初始化page,让page变为1,

import React, { useEffect, useRef } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
import { useQSState } from './useQueryString'

// useEffect只在依赖更新时执行 
export const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isInitialMount = useRef(true);

  useEffect(
    isInitialMount.current
      ? () => {
        isInitialMount.current = false;
      }
      : effect,
    deps
  );
};


// 定义 A 相关的状态和逻辑
const useA = () => {
  const [a, setA] = useQSState('a', 0) // useState 被替换成 useQSState
  const handleChangeA = () => {
    setA(a + 1)
  }
  return {
    a,
    handleChangeA
  }
}
const AContainer = createContainer(useA)

// 定义 subType 相关的状态和逻辑
const subTypeMap = {
  0: 'all',
  1: 'active',
  2: 'completed'
}
const useSubTypeSelect = () => {
  const [subType, setSubType] = useQSState('subType', 0); // useState 被替换成 useQSState
  const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
  // ReactNode
  const subTypeNode = <div>
    <h5>subType: {subTypeMap[subType]} </h5>
    <ul>
      {subTypeOptions.map((item) => (
        <li
          key={item.value}
          onClick={() => setSubType(item.value)}
        >
          {item.label}
        </li>
      ))}
    </ul>
  </div>
  // ... others
  return {
    subType,
    subTypeNode
  }
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)

// 分页逻辑
interface IPagination {
  defaultValue?: {
    page: number;
    pageSize: number;
  };
}
export const usePagination = (props: IPagination) => {
  const { defaultValue = { page: 1, pageSize: 10 } } = props || {};
  const [value, setValue] = useQSState<{
    page: number;
    pageSize: number;
  }>('pagination', defaultValue);
  const setCurrentPage = (page: number) => {
    setValue(prev => ({
      page,
      pageSize: prev.pageSize,
    }));
  };
  const setPageSize = (size: number) => {
    setValue(prev => ({
      page: prev.page,
      pageSize: size,
    }));
  };
  const resetPage = () => {
    setCurrentPage(1);
  };
  const pagination = {
    currentPage: value.page,
    pageSize: value.pageSize,
    onPageChange: setCurrentPage,
    onPageSizeChange: setPageSize,
    showSizeChanger: true,
    pageSizeOpts: [10, 20, 30, 50],
    // ...
  };
  return {
    currentPage: value.page,
    pageSize: value.pageSize,
    setPageSize,
    setCurrentPage,
    resetPage,
    pagination,
  };
};
export const PaginationContainer = createContainer(usePagination);

// 业务逻辑,跟随具体需要联动的 State 创建
const usePaginationLinkage = () => {
  const { a } = AContainer.useContainer()
  const { subType } = SubTypeSelectContainer.useContainer()
  const { currentPage, pageSize, resetPage } = PaginationContainer.useContainer();
  const _deps = [
    a,
    subType
  ];
  useUpdateEffect(() => {
    // 当 a , subType 变化时,重置分页
    resetPage();
  }, _deps);
  const deps = [..._deps, currentPage, pageSize];
  return {
    a,
    subType,
    currentPage,
    pageSize,
    deps,
  };
};
export const PaginationLinkageModel = createContainer(usePaginationLinkage);



// UI
const Main = () => {
  // 使用 A 模块相关状态和逻辑
  const { a, handleChangeA } = AContainer.useContainer()
  // 使用 subType 模块相关状态和逻辑
  const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
  // 使用 pagination 模块相关状态和逻辑
  const { currentPage, pageSize, deps } = PaginationLinkageModel.useContainer()
  return <div>
    {/* A */}
    <button onClick={handleChangeA}>A: {a}</button>
    {/* subTypeSelect */}
    {subTypeNode}
    {/* ... */}
  </div>
}


const App = () => {
  return (
    <AContainer.Provider>
      <SubTypeSelectContainer.Provider>
        <PaginationContainer.Provider>
          <PaginationLinkageModel.Provider>
            <Main />
          </PaginationLinkageModel.Provider>
        </PaginationContainer.Provider>
      </SubTypeSelectContainer.Provider>
    </AContainer.Provider>
  )
}

render(<App />, document.getElementById("root"))

请求的防抖

请求受到多个state影响,当多个state变更会导致请求触发多次,那么可以这么解决:

  1. 定义 useDebounceFn hook
import React, { DependencyList, useCallback, useEffect, useRef } from 'react';
import { useUpdateEffect } from './useUpdateEffect';

export interface ReturnValue<T extends any[]> {
    run: (...args: T) => void;
    cancel: () => void;
}
export interface IUseDebounceFnOptions {
    immediately?: boolean;
    trailing?: boolean;
    leading?: boolean;
}

/**
 * 使用防抖函数
 * @param fn - 要防抖的函数
 * @param wait - 延迟时间(默认为 1000 毫秒)
 * @param deps - 依赖项
 * @param options - 配置选项
 * @returns {ReturnValue<T>} - 返回运行函数和取消函数的对象
 */
function useDebounceFn<T extends any[]>(
    fn: (...args: T) => any,
    wait: number = 1000,
    deps: DependencyList = [],
    options: IUseDebounceFnOptions = {}
): ReturnValue<T> {
    const { immediately = false, trailing = true, leading = false } = options;
    const timer = useRef<number>();
    const haveRun = useRef<boolean>(false);
    const interval = useRef<boolean>(false);
    const fnRef = useRef<(...args: any[]) => any>(fn);
    fnRef.current = fn;

    const currentArgs = useRef<any[]>([]);

    /**
     * 设置定时器
     * @param trailing - 是否在延迟后执行函数
     * @returns {number} - 返回定时器的 ID
     */
    const setTimer = (trailing: boolean): number => {
        return window.setTimeout(() => {
            trailing ? fnRef.current(...currentArgs.current) : null;
            interval.current = false;
        }, wait);
    };

    /**
     * 取消定时器
     */
    const cancel = useCallback(() => {
        if (timer.current) {
            clearTimeout(timer.current);
        }
    }, []);

    /**
     * 运行防抖函数
     * @param args - 传递给函数的参数
     */
    const run = useCallback((...args: any[]): void => {
        currentArgs.current = args;
        if (immediately && !haveRun.current) {
            fnRef.current(...currentArgs.current);
            haveRun.current = true;
            return;
        }
        cancel();
        if (leading) {
            if (!interval.current) {
                fnRef.current(...currentArgs.current);
                interval.current = true;
            }
        }
        timer.current = setTimer(trailing);
    }, [wait, cancel]);

    useUpdateEffect(() => {
        run();
        return cancel;
    }, [...deps, run]);

    useEffect(() => cancel, []);

    return { run, cancel };
}

export default useDebounceFn;
  1. 定义 useDebounceRequest hook
export function useDebounceRequest<T>({
  fetch,
  deps = [],
  defaultValue,
}: {
  fetch: () => Promise<T>;
  deps?: any[];
  defaultValue?: any;
}) {
  const [data, setData] = useState<T>(defaultValue);
  const [loading, setLoading] = useState(false);

  // 优化:使用 async/await 处理异步操作
  const _fetchCb = async () => {
    if (loading) {
      return;
    }
    setLoading(true);
    const response = await fetch();
    setData(response);
    setLoading(false);
  };

  // 使用 useDebounceFn 进行防抖处理
  const { run } = useDebounceFn(_fetchCb, 100);

  // 监听依赖项变化时执行 run
  useEffect(run, deps);

  return { data, loading };
}
  1. 使用
import React, { useEffect, useRef } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
import { useQSState } from './useQueryString'
import { useDebounceRequest } from './useDebounceRequest';

// useEffect只在依赖更新时执行 
export const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isInitialMount = useRef(true);

  useEffect(
    isInitialMount.current
      ? () => {
        isInitialMount.current = false;
      }
      : effect,
    deps
  );
};


// 定义 A 相关的状态和逻辑
const useA = () => {
  const [a, setA] = useQSState('a', 0) // useState 被替换成 useQSState
  const handleChangeA = () => {
    setA(a + 1)
  }
  return {
    a,
    handleChangeA
  }
}
const AContainer = createContainer(useA)

// 定义 subType 相关的状态和逻辑
const subTypeMap = {
  0: 'all',
  1: 'active',
  2: 'completed'
}
const useSubTypeSelect = () => {
  const [subType, setSubType] = useQSState('subType', 0); // useState 被替换成 useQSState
  const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
  // ReactNode
  const subTypeNode = <div>
    <h5>subType: {subTypeMap[subType]} </h5>
    <ul>
      {subTypeOptions.map((item) => (
        <li
          key={item.value}
          onClick={() => setSubType(item.value)}
        >
          {item.label}
        </li>
      ))}
    </ul>
  </div>
  // ... others
  return {
    subType,
    subTypeNode
  }
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)

// 分页逻辑
interface IPagination {
  defaultValue?: {
    page: number;
    pageSize: number;
  };
}
export const usePagination = (props: IPagination) => {
  const { defaultValue = { page: 1, pageSize: 10 } } = props || {};
  const [value, setValue] = useQSState<{
    page: number;
    pageSize: number;
  }>('pagination', defaultValue);
  const setCurrentPage = (page: number) => {
    setValue(prev => ({
      page,
      pageSize: prev.pageSize,
    }));
  };
  const setPageSize = (size: number) => {
    setValue(prev => ({
      page: prev.page,
      pageSize: size,
    }));
  };
  const resetPage = () => {
    setCurrentPage(1);
  };
  const pagination = {
    currentPage: value.page,
    pageSize: value.pageSize,
    onPageChange: setCurrentPage,
    onPageSizeChange: setPageSize,
    showSizeChanger: true,
    pageSizeOpts: [10, 20, 30, 50],
    // ...
  };
  return {
    currentPage: value.page,
    pageSize: value.pageSize,
    setPageSize,
    setCurrentPage,
    resetPage,
    pagination,
  };
};
export const PaginationContainer = createContainer(usePagination);

// 业务逻辑,跟随具体需要联动的 State 创建
const usePaginationLinkage = () => {
  const { a } = AContainer.useContainer()
  const { subType } = SubTypeSelectContainer.useContainer()
  const { currentPage, pageSize, resetPage } = PaginationContainer.useContainer();
  const _deps = [
    a,
    subType
  ];
  useUpdateEffect(() => {
    // 当 a , subType 变化时,重置分页
    resetPage();
  }, _deps);
  const deps = [..._deps, currentPage, pageSize];
  return {
    a,
    subType,
    currentPage,
    pageSize,
    deps,
  };
};
export const PaginationLinkageModel = createContainer(usePaginationLinkage);

const useFetchDemoData = () => {
  const { a, subType, currentPage, pageSize } = PaginationLinkageModel.useContainer()
  const fetch = async () => {
    const res = await Promise.resolve({
      data: {
        list: [],
        total: 0
      }
    })
    // format data ...
    // do something ...
    return res
  }
  // 使用 useDebounceRequest 进行防抖
  const { data, loading } = useDebounceRequest({
    fetch,
    deps: [a, subType, currentPage, pageSize],
    defaultValue: {
      data: {
        list: [],
        total: 0
      }
    }
  })
  return {
    data,
    loading
  }
}
export const FetchDemoDataContainer = createContainer(useFetchDemoData)


// UI
const Main = () => {
  // 使用 A 模块相关状态和逻辑
  const { a, handleChangeA } = AContainer.useContainer()
  // 使用 subType 模块相关状态和逻辑
  const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
  // 使用 pagination 模块相关状态和逻辑
  const { currentPage, pageSize, deps } = PaginationLinkageModel.useContainer()
  return <div>
    {/* A */}
    <button onClick={handleChangeA}>A: {a}</button>
    {/* subTypeSelect */}
    {subTypeNode}
    {/* ... */}
  </div>
}


const App = () => {
  return (
    <AContainer.Provider>
      <SubTypeSelectContainer.Provider>
        <PaginationContainer.Provider>
          <PaginationLinkageModel.Provider>
            <FetchDemoDataContainer.Provider>
              <Main />
            </FetchDemoDataContainer.Provider>
          </PaginationLinkageModel.Provider>
        </PaginationContainer.Provider>
      </SubTypeSelectContainer.Provider>
    </AContainer.Provider>
  )
}

render(<App />, document.getElementById("root"))