likes
comments
collection
share

你应该在 React 中传递 Promise

作者站长头像
站长
· 阅读数 33

前言

展示加载态 & 错误态, 可以使用 React 自身能力

// ❌️
export default () => {
    return error ? <div>error</div>
        : loading ? <div>loading</div>
        : data
}
// ✅️ Suspense & ErrorBoundery 不一定在当前组件使用
//     会一直冒泡到最外层, 如触发页面级 loading, 而无需额外通讯
export default () => {
    return <Suspense fallback={<div>loading</div>}>
        <ErrorBoundery fallback={<div>error</div>}>
            ...
        </ErrorBoundery>
    </Suspense>
}

如何才能拿到 Promise 的加载态 & 错误态

// ❌️
export default () => {
    const [error, setError] = useState();
    const [loading, setLoading] = useState();
    const [data, setData] = useState();
    useEffect(() => {
        setLoading(true)
        fetcher('xxx').then((data) => {
            setLoading(false);
            setData(data)
        }).catch((error) => {
            setError(error);
        })
    }, []);
    return error ? <div>error</div>
        : loading ? <div>loading</div>
        : data
}
// ❌️ react-use 
export default () => {
    const { error, loading, data } = useAsync(() => fetcher(xxx), []);
    return error ? <div>error</div>
        : loading ? <div>loading</div>
        : data
}
// ✅️
const promise = fetcher('xxx');
export default () => {
    return <Suspense fallback={<div>loading</div>}>
        <ErrorBoundery fallback={<div>error</div>}>
            <Await resolver={promise}></Await>
        </ErrorBoundery>
    </Suspense>
}

谁在这么用: ReactRouter

reactrouter.com/en/main/com…

你应该在 React 中传递 Promise

他消费 Promise 的方式并不是最优的,但这不是重点。

而是我们有了另外一个选择,在 Props 中传递 Promise 而不是其返回的 data.

优势

首屏网络请求不再是副作用

// ❌️ 重新 render 会再次触发请求
export default () => {
    const resultP = useRef(fetcher('xxx'));
}
// ❌️ 重新 render 会再次触发请求
export default () => {
    const [resultP, setResultP] = useState(fetcher('xxx'));
}
// ❌️ 造成没必要的重新 render
export default () => {
    const [resultP, setResultP] = useState();
    useEffect(() => {
        setResultP(fetcher('xxx'))
    }, []);
}
// ❌️ 重发请求无法触发 render
export default () => {
    const resultP = useRef(fetcher('xxx'));
    useEffect(() => {
        resultP.current = fetcher('xxx')
    }, []);
}
// ❌️ react-use 多此一举的 useRefState
// ✅️
export default () => {
    const [resultP, setResultP] = useState(() => fetcher('xxx'));
}
// ✅️
export default () => {
    const resultP = useMemo(() => fetcher('xxx'), []);
}

如何重发请求, 状态里保存新的 Promise 就是了

// ✅️ 请求函数想调用就调用, 何必使用 react-use 要区分 useAsync / useAsyncFn / useAsyncRetry
export default () => {
    const [resultP, setResultP] = useState(() => fetcher('xxx'));
    return <>
        <FilterForm onSubmit={(filterCondition) => {
            setResultP(fetcher(filterCondition))
        }} />
        <Suspense fallback={<div>loading</div>}>
            <ErrorBoundery fallback={<div>error</div>}>
                <Await resolver={promise}>
                    <Table />
                </Await>
            </ErrorBoundery>
        </Suspense>
    </>
}

只是为了代码好看吗? 还巧妙的解决了异步竞态问题

bytedance.feishu.cn/docx/NFoddu…

// ❌️ 多次修改筛选条件造成网络请求竞态
export default () => {
    const [filterCondition, setFilterCondition] = useState();
    const [error, setError] = useState();
    const [loading, setLoading] = useState();
    const [data, setData] = useState();
    useEffect(() => {
        setLoading(true)
        fetcher(filterCondition).then((data) => {
            setLoading(false);
            setData(data)
        }).catch((error) => {
            setError(error);
        })
    }, [filterCondition]);
    return <>
        <FilterForm onSubmit={(filterCondition) => {
            setFilterCondition(filterCondition)
        }} />
        {error ? <div>error</div>
        : loading ? <div>loading</div>
        : data}
    </>
}

你应该在 React 中传递 Promise

局部加载还可以提升用户体验

www.infoxicator.com/react-route…

你应该在 React 中传递 Promise

将首屏请求提前到路由加载, 而不会阻塞页面进入

// ❌️ react-router 中的 loader 会阻塞页面进入
createBrowserRouter([
  {
    element: <Teams />,
    path: "teams",
    loader: async () => {
      return await fetcher('xxx');
    }
  },
]);
// ✅️
createBrowserRouter([
  {
    element: <Teams />,
    path: "teams",
    loader: () => {
      return defer({
          resultP: fetcher('xxx')
      });
    }
  },
]);

为什么 Promise 代表未来, React 社区推出新 api

bytedance.feishu.cn/docx/NFoddu…

// ⏸️ react unstable-api
import { use } from 'react';

const ChildComponent = () => {
    const [resultP, setResultP] = useState();
    const data = use(resultP)
    return <>
        <FilterForm onSubmit={(filterCondition) => {
            setResultP(fetcher(filterCondition))
        }} />
        <Table data={data} />
    <>;
}

export default () => {
    return <>
        <Suspense fallback={<div>loading</div>}>
            <ErrorBoundery fallback={<div>error</div>}>
                 <ChildComponent />
            </ErrorBoundery>
        </Suspense>
    </>
}
// ✅️ 基于 react-router <Await /> + useAsyncValue 的实现
const ChildComponent = () => {
    const data = useAsyncValue()
    return <>
        <Table data={data} />
    <>;
}

export default () => {
    const [resultP, setResultP] = useState();
    return <>
        <FilterForm onSubmit={(filterCondition) => {
            setResultP(fetcher(filterCondition))
        }} />
        <Suspense fallback={<div>loading</div>}>
            <Await resolver={resultP} errorElement={<div>error</div>}>
                 <ChildComponent />
            </Await>
        </Suspense>
    </>
}

使用

父子组件如何传递 Promise, 和传普通状态一样

// ✅️ 通过 Props 直接传入 promise
// ✅️ 通过 React Context 传递 promise
// ✅️ 通过状态库保存 promise

与 reduck 结合使用, 比想象的更加简洁

const fetcherModel = model('fetcher');

// ✅️
const FilterForm = () => {
    const [state, actions] = useModel(fetcherModel);
    return <Form>
        <Button onClick={() => actions.setState(fetcher('xxx'))}}></Button>
    </Form>
}

// ✅️
const ShowData = () => {
    const [state] = useModel(fetcherModel);
    return <Suspense fallback={<div>loading</div>}>
        <ErrorBoundery fallback={<div>error</div>}>
            <Await resolver={state}></Await>
        </ErrorBoundery>
    </Suspense>
}
// ✅️ 状态库将不再有副作用
const fetcherModel = model('fetcher').define({
    state: {
        resultP: null
    },
    computed: {
        some(state) {
            return state.resultP?.then((data) => {
                return ... //数据处理
            })
        }
    },
    actions: {
        doSomeThing(state) {
            state.resultP = fetcher('xxx')
        },
        // ❌️ 省去多余状态
        load: handleEffect({ result: 'items', pending: false, error: false }),
    },
    // ❌️ 不再有副作用
    effects: {}
})

进阶

我的 React / ReactRouter 版本不够怎么办,自己实现

// 基于 loadable 实现 <Await /> & useAsyncValue();
// ⏸️ 仅供参考, 推荐下文中基于 use() api 的实现
import loadable from "@loadable/component";
import React, { ReactNode, createContext, useContext } from "react";

const AsyncDataContext = createContext<unknown>(undefined);

export const Await = loadable(
  async (props: {
    resolver: Promise<unknown>;
    children?: ReactNode | ((data: any) => ReactNode);
  }) => {
    const { resolver } = props;
    const data = await resolver;
    return (props) => {
      const { children } = props;

      if (typeof children === "function") {
        return children(data);
      }

      return (
        <AsyncDataContext.Provider value={data}>
          {children}
        </AsyncDataContext.Provider>
      );
    };
  },
  {
    cacheKey: ({ resolver }) => resolver,
  }
) as unknown as <T>(props: {
  resolver: Promise<T>;
  children?: ReactNode | ((data: T) => ReactNode);
}) => ReactNode;

export const useAsyncValue = () => {
  return useContext(AsyncDataContext);
};

实现 use() api

// ✅️
function use(promise) {
  if (promise.status === 'fulfilled') {
    return promise.value;
  } else if (promise.status === 'rejected') {
    throw promise.reason;
  } else if (promise.status === 'pending') {
    throw promise;
  } else {
    promise.status = 'pending';
    promise.then(
      result => {
        promise.status = 'fulfilled';
        promise.value = result;
      },
      reason => {
        promise.status = 'rejected';
        promise.reason = reason;
      },      
    );
    throw promise;
  }
}

基于 use() api 实现 <Await />

// ✅️ 这个可以取代 loadable 的实现版本
export const Await = <T>(props: {
  resolver: Promise<T>;
  children?: ReactNode | undefined | ((data?: T) => ReactNode);
}) => {
  const { resolver, children } = props;
  const data = use(resolver);
  if (typeof children === 'function') {
    return children(data);
  }
  return children;
};

例外, 我就是要获取 loading 状态怎么办

// ❌️ react-use 何必要区分 useAsync / useAsyncFn / useAsyncRetry
export default () => {
    const { error, loading, data } = useAsync(() => fetcher(xxx), []);
    return <Loading loading={loading}>
        <Table />
    </Loading>
}
// ✅️
const promise = fetcher('xxx');
export default () => {
    const { error, loading, data } = usePromise(promise);
    return <Loading loading={loading}>
        <Table />
    </Loading>
}
// ✅️ 实现 usePromise()
type PromiseCanUse<T> = Promise<T> & {
  status: 'pending' | 'fulfilled' | 'rejected';
  reason: unknown;
  value: T;
};
function usePromise<T>(promise?: PromiseCanUse<T>) {
  const [_, forceUpdate] = useState({});
  const ref = useRef<PromiseCanUse<T>>();
  if (!promise) return { loading: false, data: promise };
  ref.current = promise;
  if (!promise.status) {
    promise.status = 'pending';
    promise
      .then(
        result => {
          promise.status = 'fulfilled';
          promise.value = result;
        },
        reason => {
          promise.status = 'rejected';
          promise.reason = reason;
        },
      )
      .finally(() => {
        setTimeout(() => {
          if (ref.current === promise) {  // 最新的 promise 才会触发 forceUpdate
            forceUpdate({});
          }
        }, 0);
      });
  }
  return {
    loading: promise.status === 'pending',
    data: promise.value,
    error: promise.reason,
  };
}

总结

传统网络请求创建 Promise 后, 会立即使用 useEffect 消费掉 Promise 转换成 loading & data || error.

而现在, 你应该在组件内或跨组件传递 Promise.

你应该在 React 中传递 Promise

⭐️⭐️⭐️ 在你的项目中使用

// ✅️ 一个文件导出 usePromise() / use() / <Await />

import { ReactNode, createContext, useContext, useRef, useState } from 'react';

type PromiseCanUse<T> = Promise<T> & {
  status?: 'pending' | 'fulfilled' | 'rejected';
  reason?: unknown;
  value?: T;
};

/**
 * 在当前组件使用 loading/error
 */
export function usePromise<T>(promise?: PromiseCanUse<T>) {
  const [_, forceUpdate] = useState({});
  const ref = useRef<PromiseCanUse<T>>();
  if (!promise) return { loading: false, data: promise };
  ref.current = promise;
  if (!promise.status) {
    promise.status = 'pending';
    promise
      .then(
        result => {
          promise.status = 'fulfilled';
          promise.value = result;
        },
        reason => {
          promise.status = 'rejected';
          promise.reason = reason;
        },
      )
      .finally(() => {
        setTimeout(() => {
          if (ref.current === promise) {
            forceUpdate({});
          }
        }, 0);
      });
  }
  return {
    loading: promise.status === 'pending',
    data: promise.value,
    error: promise.reason,
  };
}

/**
 * 在父级/祖父级组件中使用 Suspense/ErrorBoundery 接收 loading/error
 */
export function use<T>(promise?: PromiseCanUse<T>) {
  if (!promise) return promise;
  if (promise.status === 'fulfilled') {
    return promise.value;
  } else if (promise.status === 'rejected') {
    throw promise.reason;
  } else if (promise.status === 'pending') {
    throw promise;
  } else {
    promise.status = 'pending';
    promise.then(
      result => {
        promise.status = 'fulfilled';
        promise.value = result;
      },
      reason => {
        promise.status = 'rejected';
        promise.reason = reason;
      },
    );
    throw promise;
  }
}

const AsyncDataContext = createContext<unknown>(undefined);

/**
 * 在当前组件或父级/祖父级组件中使用 Suspense/ErrorBoundery 接收 loading/error
 */
export const Await = <T,>(props: {
  resolver?: Promise<T>;
  children?: ReactNode | undefined | ((data?: T) => ReactNode);
}) => {
  const { resolver, children } = props;
  const data = use(resolver);
  if (typeof children === 'function') {
    return children(data);
  }
  return (
    <AsyncDataContext.Provider value={data}>
      {children}
    </AsyncDataContext.Provider>
  );
};

/**
 * 在当前组件接收来自父级 <Await /> 组件的 data
 * @deprecated 不推荐使用, 会丢失 ts 类型
 */
export const useAsyncValue = () => {
  return useContext(AsyncDataContext);
};

实例演练

  1. 实现按钮的权限控制

使用场景

const MyComponent = () => {
    const xPermission = usePermission('xButton');
    
    return <div>
        ...someThing 展示
        {xPermission ? <Button onClick={...}>x</Button>}
    </div>
}

实现

设 fetchPermission 为获取权限的网络请求

import { createContext, useContext, useMemo, use } from 'react';

const PermissionContext = createContext(undefined);

export const Provider = () => {
  const promise = useMemo(() => fetchPermission(), []);
  return <PermissionContext.Provider value={promise}></PermissionContext.Provider>;
};

export const usePermission = (functionCode?: string) => {
  const promise = useContext(PermissionContext);
  const permissions = use(promise); // loading 会由上层最近的 Suspense 承接.
  if (!functionCode) return true;
  return permissions?.find(functionCode)
}

如果你想用 set 来优化数组性能

import { createContext, useContext, useMemo } from 'react';

const PermissionContext = createContext(undefined);

export const Provider = () => {
  const promise = useMemo(
    () => fetchPermission().then(permissions => new Set(permissions)),
    [],
  );
  return <PermissionContext.Provider value={promise}></PermissionContext.Provider>;
};

export const usePermission = (functionCode?: string) => {
  const promise = useContext(PermissionContext);
  const permissionSet = use(promise);
  if (!functionCode) return true;
  return permissionSet?.has(functionCode)
}

一个 hook 实现, 避免将逻辑散落在状态管理.

杂谈

你真的会使用 await 吗, 你的行为可能正在劣化性能

// ❌️
export default async () => {
    await fetcher('some1')
    await fetcher('some2')
}
// ✅️
export default async () => {
    return Promise.all([fetcher('some1'), fetcher('some2')])
}

注意事项

use(promise) 能够在 loading 结束后正常渲染的前提是loading前后的 promise 需要是同一个值,而 use(Promise.all(xxx)) 会在每次 render 的时候创建一个新的 promise 就会一直触发 loading 态。

解决方案是把会生成新 Promise 的运算都放在一个 useMemo 中。

扩展阅读

转载自:https://juejin.cn/post/7268187099525349431
评论
请登录