采用策略模式书写ts友好的react组件

4,318 阅读3分钟

业务逻辑里,如何干掉if else&switch case渲染组件并且对ts友好的语句呢,让我们来一起探讨吧,看看是否你也曾经是这样处理过。

处理多种渲染结果

考虑我们的代码里有以下3钟完全不一样的组件FooBarBaz,为了可以贴合真实情况的复杂度,我们的演示的组件不仅渲染结果不一样,输入的参数props也是不一样的.

interface IPropsFoo {
  id: string;
}
const Foo = React.memo((props: IPropsFoo) => {
  return <h1>{props.id}</h1>
})


interface IPropsBar {
  name: string;
}
const Bar = React.memo((props: IPropsBar) => {
  return <h1>{props.name}</h1>
})


interface IPropsBaz {
  age: number;
}
const Baz = React.memo((props: IPropsBaz) => {
  return <h1>{props.age}</h1>
})

在处理一些单选按钮组RadioGroup或者可切换页卡Tab组件时,通常会根据不同的选中key渲染不同的结果,偶们大多数时候大笔一挥会洒脱写下以下代码。

function Demo1() {
  let initialKey: 1 | 2 | 3 = 1;
  const [key] = React.useState(initialKey);
  const [state] = React.useState({ id: '', name: '', age: 1 });

  let ui: React.ReactNode = '';
  if (key === 1) {
    ui = <Foo id={state.id} />;
  } else if (key === 2) {
    ui = <Bar name={state.name} />;
  } else if (key === 3) {
    ui = <Baz age={state.age} />;
  }

  return <h1>
    {ui}
  </h1>;
}

分析行为,创建策略池

显而易见这里的if else分支语句会和key的枚举数量呈现正相关增长关系(10个单选项孵化出10个 if else分支语句),函数的圈复杂度也就很容易随之而上升了,这时候策略模式就排上用场了,我们分析下此场景,可以把key当前行为,渲染实例当做输出结果,那么就很容易写个策略池来描述不同的key应该命中的输出结果啦。

const compUis = {
    1: <Foo id={state.id} />,
    2: <Bar name={state.name} />,
    3: <Baz age={state.age} />,
  };

  return <h1>
    {compUis[key]}
  </h1>;

更高效的运行效率

或许大多数时候,我们写出上面的代码就认为已经可以交差了,但我们如果追求更高效的运行效率,是不应该把所有组件都提前实例化好,然后在根据key去具体命中的,而是惰性的在命中那一刻才实例化,所以我们可以继续优化为

const compUis = {
    1: () => <Foo id={state.id} />,
    2: () => <Bar name={state.name} />,
    3: () => <Baz age={state.age} />,
  };

  return <h1>
    {compUis[key]()}
  </h1>;

react/no-unstable-nested-components

上面代码看起来很完美,利用箭头函数包装一下实例,延迟了实例化时机,但是经过eslint校验会给出react/no-unstable-nested-components,告诉我们不允许在组件内部临时申明不稳定的组件,这可能会引起一些莫名的bug(通常由闭包引起),具体说明可见链接

这时候,我们仔细回想一下jsx语法,<Some />语法糖经过babel编译后其实是React.createElement(Some)语句,所以我们变通下,把组件的元数据提取出来描述清楚即可。

const compMetas = {
    1: { Comp: Foo, props: { id } },
    2: { Comp: Bar, props: { name } },
    3: { Comp: Baz, props: { age } },
  };
  const meta = compMetas[key];

  return <h1>
    {<meta.Comp {...meta.props} />}
  </h1>;

丢失的类型校验

到此为止我们解决掉了react/no-unstable-nested-components问题,可是组件的类型校验却完全丢失了,Foo组件的props声明我们可以随便写错,ts也不会帮我们发现错误,所以呢我们还需要补齐类型声明,让不同的组件Comp类型与props类型关联起来

const compMetas: {
    1: { Comp: typeof Foo, props: Parameters<typeof Foo>[0] },
    2: { Comp: typeof Bar, props: Parameters<typeof Bar>[0] },
    3: { Comp: typeof Baz, props: Parameters<typeof Baz>[0] },
  } = {
    1: { Comp: Foo, props: { id } },
    2: { Comp: Bar, props: { name } },
    3: { Comp: Baz, props: { age } },
  };
  const meta = compMetas[key];

  return <h1>
    {<meta.Comp {...meta.props} />}
  </h1>;

更精简的类型推导

经过以上改造,ts不会让我们随便写错props值了,可以我们发现这样的类型推导好冗余,可以稍稍再改造下,让它变得更精简吧^_^

type CompMeta<C extends
  (React.NamedExoticComponent<any> & { readonly type: (props: any) => JSX.Element; })
  > = { Comp: C, props: Parameters<C>[0] };


const compMetas: {
    1: CompMeta<typeof Foo>,
    2: CompMeta<typeof Bar>,
    3: CompMeta<typeof Baz>,
  } = {
    1: { Comp: Foo, props: { id } },
    2: { Comp: Bar, props: { name } },
    3: { Comp: Baz, props: { age } },
  };
  const meta = compMetas[key];

结语

经过以上操练,你学会如何在ts里结合策略模式书写react组件了吗^_^

one more thing, 如果你想在react里尝试类vue书写体验,composition api 等有趣特性,欢迎❤ star concent^_^