还在层层传递props?来学学非常实用的供应商模式吧

lxf2023-03-15 10:34:01

本文正在参加「 . 」,欢迎大家来交流。

背景介绍

这是设计模式系列的第三节,学习的是patterns.dev里设计模式中供应商模式内容,由于是资料是英文版,所以我的学习笔记就带有翻译的性质,但并不是翻译,记录的是自己的学习过程和理解

第一节:高并发造成的数据统计困难?看我单例模式一招制敌

第二节:JS和迪丽热巴一样有专业替身?没听过的快来补补课...

写在前面

供应商模式可以说非常实用,在组件设计拆分过程中,很多不同层级的组件需要全局的用户信息,或者某个局部业务数据,有时我们懒于设计,采用最懒最直接的props层层传递的模式实现,经常我们是迫于项目排期,或者没想到更好的方式,或者不熟悉供应商模式,最终都造成了我们不愿意看到的一个事实:写了难以维护代码,俗称shi山

过段时间再来维护自己的代码.gif,打死都不愿承认这些代码自己的杰作

如果你也有这样的经历,来吧,跟我一起学习供应商模式,不要在代码里玩低端props接力了!!

释义

像供应商一样,为不同层级子组件供应全局或者局部数据;

shi山代码分析

比如一个App页面,有侧边栏SideBar组件和内容Content组件;SideBar组件内是个列表组件List,List组件内有很多子项ListItem,需要用到App页面的data数据;内容Content组件内有HeaderBlock组件,都需要用到App里的data数据。

如果层层传递的写法,伪代码如下:

function App() {
  const data = { ... }
  
  return (
    <div>
      <SideBar data={data} />
      <Content data={data} />
    </div>
  )
}

const SideBar = ({ data }) => <List data={data} />
const List = ({ data }) => <ListItem data={data} />
const ListItem = ({ data }) => <span>{data.listItem}</span>

const Content = ({ data }) => (
  <div>
    <Header data={data} />
    <Block data={data} />
  </div>
)
const Header = ({ data }) => <div>{data.title}</div>
const Block = ({ data }) => <Text data={data} />
const Text = ({ data }) => <h1>{data.text}</h1>

这样层层传递层级够深,就形成props黑洞。上面的示例代码如果在真实的项目,会一个组件一个文件,复杂的情况可能要跨好几层文件夹,有些层级完全不消费data数据,也必须要代为子组件传递props;再者假如哪天修改一下data里的结构或者属性名,真的很头疼,这么多文件,哪个改了,哪个没改,还真不好确定,一点简单的改动,头上的头发估计又要掉好几根...

还在层层传递props?来学学非常实用的供应商模式吧

绝顶聪明

接下来,有请我们实力派供应商模式上场!

供应商模式

确实,遇到这种需要跨层级传递props的情况,很适合我们供应商模式。供应商模式像是一个商店一样,为不同层级的组件提供所需要的props商品

那具体要怎么做呢?用一个Provider包裹所有需要使用data数据的组件,它是通过Context上下文进行传递的,这就要用到React为我们提供的createContext方法了。

const DataContext = React.createContext()

function App() {

const data = { ... }

return (
    <div>
       <DataContext.Provider value={data}>
            <SideBar />
            <Content />
      </DataContext.Provider>
    </div>
  )
}

上面的代码,我们调用createContext方法创建了DataContext上下文对象,然后我们实用DataContext.Provider包裹所有需要使用data数据的组件,并通过value属性把data传过去。

这样我们完成了供应商模式的第一步了,有了供应商子组件要怎么获得data数据呢?

const SideBar = () => <List />
const List = () => <ListItem />
export const Content = () => <div><Header /><Block /></div>


function ListItem() {
  const { data } = React.useContext(DataContext);//跨文件 import导入DataContext
  return <span>{data.listItem}</span>;
}

function Text() {
  const { data } = React.useContext(DataContext); 
  return <h1>{data.text}</h1>;
}

function Header() {
  const { data } = React.useContext(DataContext);
  return <div>{data.title}</div>;
}

大家可以看到,我们实用React为我们提供的useContext Hook就可以拿到data数据;通过这种改造,就有效避免了每个层级都传props,使用就消费,不使用就可以完全忽略data数据,后面修改或者重构起来也轻松多了。

所以说供应商模式是非常有用,特别是在共享全局或局部数据的时候。

经典案例

接下来我们来看一个经典案例:动态主题 ———— 点击按钮切换主题颜色。其他定制主题或者多主题模式的场景实现方法大概差不多,多数是在多种身份或者角色的系统中,比如说一个项目中有买家卖家两种角色,在产品设计时买家用橙色主题色,卖家用蓝色主题色,这时就可以使用供应商模式,设置全局主题供应商Provider,根据用户身份动态设置主题色。

主题切换Demo在线体验:codesandbox.io/embed/quirk…, 下面来分析下里面的代码。

根组件App.js中:

export const ThemeContext = React.createContext();

const themes = {
  light: {
    background: "#fff",
    color: "#000"
  },
  dark: {
    background: "#171717",
    color: "#fff"
  }
};

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  const providerValue = {
    theme: themes[theme],
    toggleTheme
  };

  return (
    <div className={`App theme-${theme}`}>
      <ThemeContext.Provider value={providerValue}>
        <Toggle />
        <List />
      </ThemeContext.Provider>
    </div>
  );
}

我们首先调用createContext创建出一个ThemeContext并导出。然后使用useState生成theme主题变量,然后通过ThemeContext.Provider包裹需要修改主题的组件。

然后在子孙组件里使用useContext消费主题,代码大概如下:

// Toggle.tsx
import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function Toggle() {
  const theme = useContext(ThemeContext);

  return (
    <label className="switch">
      <input type="checkbox" onClick={theme.toggleTheme} />
      <span className="slider round" />
    </label>
  );
}
// ListItem.tsx
import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function ListItem() {
  const theme = useContext(ThemeContext);

  return <li style={theme.theme}>...</li>;
}

这里由于List组件本身并不消费theme,那么就可以完全忽略theme,而ListItem组件需要消费theme,则直接通过useContext消费;更进一步还可以在ListItem里,通过toggleTheme方法修改全局主题。

这如果使用props传递模式,那也是要通过List组件传递toggleTheme方法。

所以说供应商模式可以说非常的实用,一定要在项目中用起来

封装供应商模式HOOK

上面的案例中,我们通过React.createContext和React.useContext方法创建和消费供应商Context,需要子组件导入Context;这里我们可以自己封装一个hook,就可以简化使用useContext的逻辑

主题的逻辑相对比较独立,我们可以把这块内容单独提出来,其实可以直接讲主题切换的逻辑封装在ContextProvider里:

function ThemeProvider({children}) {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  const providerValue = {
    theme: themes[theme],
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={providerValue}>
      {children}
    </ThemeContext.Provider>
  );
}

export default function App() {
  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider>
        <Toggle />
        <List />
      </ThemeProvider>
    </div>
  );
}

然后我们封装下消费ThemeContext主题的逻辑:

function useThemeContext() {
  const theme = useContext(ThemeContext);
  if (!theme) {
    throw new Error("useThemeContext must be used within ThemeProvider");
  }
  return theme;
}

经过这一步简单封装之后,消费Context时,直接调用useThemeContext钩子,组件就能拿到theme了。

优秀实现案例:style-components里的ThemeProvider

不知道大家有没有使用过style-components组件,里面的ThemeProvider就是类似的实现逻辑,有兴趣的可以找找源码阅读一下:


import { ThemeProvider } from "styled-components";

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider theme={themes[theme]}>
        <>
          <Toggle toggleTheme={toggleTheme} />
          <List />
        </>
      </ThemeProvider>
    </div>
  );
}

import styled from "styled-components";

export default function ListItem() {
  return (
    <Li>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat.
    </Li>
  );
}

const Li = styled.li`
  ${({ theme }) => `
     background-color: ${theme.backgroundColor};
     color: ${theme.color};
  `}
`;

总结:

供应商模式 ———— React中Context相关的API,实现了跨层级传递props。并且有效减少重构代码时发生bug的几率,不消费某个数据的组件可以完全忽略这个数据向下传递,同时也让整个应用的数据流变的更清晰可控

根目录上使用一些供应商模式,会让整个应用的数据流都变的清晰起来,因为所有组件都能轻松获取全局的变量

当然供应商模式也有缺点过度使用供应商模式时,所有useContext关联数据的组件,都会在数据变化时重新渲染,这会影响应用的性能

所以要确保不消耗某数据的组件,不会因为使用了useContext,从而产生该数据变化而重新渲染,那就可能需要拆分不同的Provider供应商来针对不同数据的更新,从而能避免无效渲染,最大限度提升性能