[React] How to trigger global component by calling API?

Scenario

There're many times/cases we want to trigger a global component by calling an API, e.g. user picker component, in anywhere. Usually, we need to place the UserPicker in where we want to show/hide, and add some code for getting selected result. Something like:

{
  const [visible, setVisible] = useState(false)
  return (
    <div>
      <Button onClick={() => setVisible(true)} />
      <Modal visible={visible} onOK={() => {...}} onCancel={() => setVisible(false)}>
        <UserPicker onSelected={(users) => {...}} />
      </Modal>
    </div>
  )
}

But the problem is:

  • You need to place UserPicker in wherever you need to pick user, similar UI design and similar logic just to fetch the selected users ... that sounds like something reusable.
  • And even worse, when you pack most of the reusable code into hooks, and eventually found that hooks cannot handle the UI part, but you can't live without hooks. It's just like a torture between ice and fire.
    We can expose an API from hooks for user to call, but we still need to handle the UI component. One of the ways for UI is to use a "global" component, which is just being placed once and can be "shared" everywhere, then we can have what we want, simplified and reusable code. Ok, let's try to figure it out.
How to do it with API?

Here introduce some graceful way to do this in react, and take UserPicker as an example. The scenario is as below:

There're several places that we need to pick users, and we'd like to trigger this UserPicker by a simple API call, and use a callback to fetch the select result.

  • Wrap UserPicker in top level component

    Let's say TablePage is the root container that accommodates all the other components, such as buttons, menu... So we can trigger the UserPicker in any of these components.

{
  return (
    <div>
      <TablePage ... />
      <UserPicker ... />
    </div>
  )
}
  • Have an indicator to show/hide UserPicker, to resolve the UI part. Ok, let's add some more code.
// TablePage
{
  const [show, setShow] = useState(false)

  return (
    <div>
      {children}
      <Modal visible={show} onOk={() => { ... }} onCancel={() => setShow(false)}>
         <UserPicker ... />
      </Modal>
    </div>
  )
}
  • But how to "show" the Modal, someone gotta call setShow(true). We can use callback to do it, but that's not gonna be pretty, ugly code ... wired logic ..., we don't want that.
    "Global" data is the better way for it. Of course you can use redux/dva/whatever workable. But here we just want to use some "lightweight" method which can be used for inter-components communication, "Context" which is one of native feature provided by react.
    I'm not gonna liberate every detail on how to do it, I'm just gonna use some off-the-shelf and place the code here (not all of them). Let's re-design the code a little bit.
// TablePage
{
  const { show, setShow } = useModel('ui') // global data getter/setter

  return (
    <div>
      {children}
      <Modal visible={show} onOk={() => { ... }} onCancel={() => setShow(false)}>
         <UserPicker ... />
      </Modal>
    </div>
  )
}

Wherever user call setShow(true) will show the UserPicker and the buttons "OK" and "Cancel" can hide UserPicker. Seems like we've done the first step, UI part.

  • API
    What we want is simply calling an API and then trigger and fetch the selected result. Ok, that's pretty straightforward. Let's design the API.
const { show, setShow } = useModel('ui')
type CB = (params: {users: UserInfo[]}) => void

export function showUserPicker(cb: CB) {
  setShow(true)
}

We're quite close, but there's still one problem left. How can we get the selected result back? Obviously we need some way to pass our callback to UserPicker. Use "global" getter/setter again? Maybe, but that doesn't sound like a natural way, because how can we know user has selected and click "OK"? That's something "event" does. Ok, we need to use an event system. Or, anyway that can "notify" you when user actions.

  • Event
    We're gonna use some off-the-shelf package to do it, useEventEmitter from ahooks. Here is the sample code:
export interface Event {
  name: string
  cb: CB
}

const event$ = useEventEmitter<Event>()

// for the emitter
event$.emit({
  name: 'showUserPicker'
  cb: (params: { users: UserInfo[] }) => { ... }
})

// for the subscriber
event$.useSubscrption((event) => {
  const { name, cb } = event
  ...
})

And since we want to share the same event$ in between components, so we need to put it into "global" data. Let's say in event model. We can fetch it by:

  const { event$ } = useModel('event')
  • Assemble together
    Now let's connect all the part together.

    UI

// TablePage
{
  const { show, setShow } = useModel('ui') // global data getter/setter
  const { event$ } = useModel('event')
  const [selectedUsers, setSelectedUsers] = useState<UserInfo[]>([])
  const callback = useRef<any>()

  event$.useSubscription((evt) => {
    const { name, cb } = evt
    callback.current = cb
    setShow(true)
  }

  return (
    <div>
      {children}
      <Modal 
        visible={show} 
        onOk={() => {
          callback.current?.(selectedUsers)
        }} 
        onCancel={() => setShow(false)}
      >
        <UserPicker onSelected={ (users) => {
          setSelectedUsers(users)
        } } />
      </Modal>
    </div>
  )
}

API

type UserPickerCb = (params: { users: UserInfo[] }) => any

export function useUserPicker() {
  const { event$ } = useModel('event')
  const userPicker = useCallback(
    (cb: UserPickerCb): any => {
      event$.emit({ name: EVT_SHOW_USER_PICKER, cb })
      return true
    }, [])

  return { userPicker }
}

// caller
userPicker(
  ({ users }) => {
    // fetch "users" after user confirm, then we can do the rest work here
    // TODO
  }
)

Ok, we've done all the work here. But we can take one more step further to make it more reusable. Say, maybe we have another root container, name ListPage which probably need to use UserPicker too, and others ... We definitely don't want to write the similar code again for ListPage. Let's design a wrapper to pack the "UI part" for reuse, and name it UserPickerWrapper.

// UserPickerWrapper.tsx
interface Props {
  children: React.ReactNode
}

const UserPickerWrapper: React.FC<Props> = (props) => {
  const { children } = props
  const [showUserPicker, setShowUserPicker] = useState(false)
  const [selectedUsers, setSelectedUsers] = useState<UserInfo[]>([])
  const { event$ } = useModel('event')
  const callback = useRef<any>()

  event$.useSubscription((evt) => {
    const { name, cb } = evt
    callback.current = cb
    console.log('WidgetWrapper useSubscription name:', name)
    if (name === EVT_SHOW_USER_PICKER) {
      setShowUserPicker(true)
    }
  })

  return (
    <div>
      {children}
      <Modal
        visible={showUserPicker}
        onOk={() => {
          // TODO: pass result to caller
          callback.current?.({ users: selectedUsers })
          setShowUserPicker(false)
        }}
        onCancel={() => {
          callback.current?.({ users: [] })
          setShowUserPicker(false)
        }}
      >
        <UserTree
          onSelected={(nodes) => {
            const users: UserInfo[] = (nodes as UserDataNode[]).map((node) => ({
              emplId: node.emplId,
              name: node.name,
              avatar: node.avatar,
            }))
            setSelectedUsers(users)
          }}
        />
      </Modal>
    </div>
  )
}

export default UserPickerWrapper

As for TablePage, re-write the code:

export default TablePage {
  return (
    <UserPickerWrapper>
    { ... }
    </UserPickerWrapper>
  )
}

As for ListPage, write the code:

export default ListPage {
  return (
    <UserPickerWrapper>
    { ... }
    </UserPickerWrapper>
  )
}

Wherever in TablePage or ListPage, just call showUserPicker(...) in whatever component and you can show UserPicker for user to select and get back result for further processing.

And of course, you can extend this to more scenarios.
I guess we can have more coffee time now.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容