preface

Wechat small program provides a lot of similar TO Wx. showModal, wx.showLoading such API, although this API is convenient to use, but the style is ugly, often does not meet our needs.

Is there any way for our custom popovers and loading to be called at will through API like wechat?

Let’s start with the renderings:

You can see that just one button is written on the Index page to trigger the popover.

To summarize the full text:

  • Use the script to generate a popover configuration for each popover file
  • According to the popover configuration, the Popover Wrapper component maps each popover
  • Use webpack-Loader to inject wrapper components into each page
  • Use Chokidar to listen for popover file creation, automatically run scripts, and update popover configuration
  • Design an API-style call style

This post is based on a year and a half of developing applets using the Taro ReactHook TypeScript technology stack. It may not be universal, but it’s great if it can give you some thought and insight.

The target

First look at the features:

wx.showModal({
  title: "Tip".content: "Operation illegal"});Copy the code
  • 1, API-style call.
  • 2, global. In the small program anywhere can be called

The goals of this paper are mainly these two points.

Api-style call

When making such a call, we need to pass data and state into a component in some way, and the component responds.

The ways to pass data are props and context.

The props scheme is ruled out first, because you can’t pass props to popovers for every component.

What about using the context scheme? To use context, you need to provide a Provider at the top of the application, pass all popover data and explicit and implicit states, and methods to modify the data into the Provider, and map all popover data in the Consumer. Where popovers need to be turned on or off, use this.context or useContext to get the data modification method and then control the popover state.

A pseudocode using the Context scheme is provided below

const ModalContext = createContext(null) <! ---- app.tsx entry file ---->export default function (props) {
  const [config, setConfig] = useState(modals)
  return (
    <ModalContext.Provider value={{config, setConfig}} >
      {props.children}
    </ModalContext.Provider>)}, <! ---- index.tsx home page ---->export default function () {
  const { setConfig } = useContext(ModalContext)

  return (
    <View>{/* *}<View onClick={()= >SetConfig ((d) => {name: 'a',visible: true, data: 1})}> Open a popover</View>
      <Wrapper />
    </View>)}, <! ---- wrapper. TSX popup Wrapper component ---->export function Wrapper() {
  return (
    <ModalContext.Consumer>
      {
        ({config}) => {
          return (
            <>{config.map(c => {return (// each popover instance<Modal data={c} />)})}</Modal>
          )
        }
      }
    </ModalContext.Consumer>
  )
}
Copy the code

For wrapper components that need to be imported into every page file, using useContext for popovers is acceptable, but be careful to optimize, as any setConfig will cause components or pages to be re-rendered from useContext(ModalContext).

How to avoid this problem?

If you can store the top-level setConfig externally, and import setConfig method calls from outside each time, without using useContext directly, along with memo optimization, you can solve the re-rendering problem.

The pseudocode is as follows:

<! ---- useStore custom hook ---->export let setModalConfig

export function useStore(initValue) {
  const [config, setConfig] = useState(initValue)
  setModalConfig = setConfig
  return[config, setConfig] } <! ---- app.tsx entry file ---->export default function (props) {
  const [config, setConfig] = useStore(modals)
  return (
    <ModalContext.Provider value={{config, setConfig}} >
      {props.children}
    </ModalContext.Provider>)}Copy the code

To open the popover, simply call setModalConfig directly.

If we store each useState data and setData externally and assign it an identity, then we can retrieve data and methods in useState anywhere based on that identity.

Based on this, we packaged stook, an easy-to-use state management tool

Simple implementation is as follows:

export const stores[] = []

// Change the hook state externally
export function mutate(key, value) {
  const cacheIdx = stores.findIndex(store= > store.key === key)
  stores[cacheIdx].cbs.forEach(cb= > cb(value))
}

// Externally get the state of hooks
export function getState(key) {
  const cacheIdx = stores.findIndex(store= > store.key === key)
  return stores[cacheIdx].value
}

export function useStore(key, initValue? :any) {
    const cache = stores.find(store= > store.key === key)
    // For a useState with the same key, first try initialization with cached data
    const[state, setState] = useState(cache? .value || initValue)// Prevent the same setState from being cached multiple times
    if(! cache? .cbs.find(cb= > cb === setState)) {
      if(! cache) {// For keys with the same name, the value should be the same, and each function that changes the state needs to be saved
        stores.push({ key, value: state, cbs: [setState] })
      } else {
        cache.cbs.push(setState)
      }
    }

    useEffect(() = > {
      // Component or page unmount
      return () = > {
        const cacheIdx = stores.findIndex(store= > store.key === key)
        constidx = stores[cacheIdx]! .cbs.findIndex(cb= >cb === setState) cache! .cbs.splice(idx,1)
        if(! cache? .cbs.length) { stores.splice(cacheIdx,1)
        }
      }
    }, [key, setState])

    return [
      state,
      function (value) {
        let newValue = value
        if (typeof value === 'function') {
          newValue = value(state)
        }
        const cache = stores.find(store= > store.key === key)!
        cache.value = newValue
        cache.cbs.forEach(cb= > cb(value))
      }
    ]
}
Copy the code

With the state management tools we have designed, we can discard the context altogether.

<! ---- app.tsx entry file ---->export default function (props) {
  returnprops.children } <! ---- index.tsx home page ---->export default function () {
  return (
    <View>
      <View onClick={()= >Mutate ('modal', (d) => {})}> Open the A popover</View>
      <Wrapper />
    </View>)}, <! ---- wrapper. TSX popup Wrapper component ---->export function Wrapper() {
  const [modalConfig] = useStore('modal', config)
  return (
    <>
      {
        modalConfig.map(c => {
          return (
            <Modal data={c} />)})}</>)}Copy the code

With this foundation, a lot can be done through apI-style call popovers

class Service {
  openCommonModal(data) {
    mutate("modal".(config) = > {
      const commonConfig = config.find(({ name }) = > name === "common");
      commonConfig.visible = true;
      commonConfig.data = data;
      return[...config]; }); }}export const service = new Service();
Copy the code

In this case, you can open pop-ups using service.openCommonModal.

The global call

There is no way to define a global component in an applet, only to introduce components into each page. With Webpack-Loader, we can implement the ability to automatically inject components per page.

We designed a Webpack-loader to do this taro-inject-component-loader

Each page after injection introduces a popover component, so the Service popover can be called anywhere.

automation

We’ve been using something called Config, which is like a routing table file and is a mapping table of Modal components and names

const config = [
  {
    name: "common".component: CommonModal,
  },
];
Copy the code

For easy maintenance, you are advised to place all modal files in a folder.

|---src
|----|----modals
|----|------|------common.tsx
|----|------|------loading.tsx
|----|------|------actionSheet.tsx
Copy the code

In this case, a script tool can be used to walk through the folder and automatically generate a configuration file. The effect is similar to that of UMI exclusive routing. In addition, you can use Chokidar to listen for the deletion and creation of files in the Modals folder to automatically update the Config configuration file.

It’s not just the config file. If you look closely at the service file, you can use a script to automatically generate a service file.

Our team designed and implemented a set of scripting tools to accomplish this function.

  • Generated. A CLI tool used to run configuration script files
  • generated-plugin-taro-modal-service. Ts – MORph was used to generate popover configuration files.
  • Webpack-plugin-chokidar. Combines WebPack and Chokidar to optimize the API design used in WebPack

conclusion

I hope this article can help you.

Originally published on my blog: Taro Custom showModal

The templates covered in this article are here.