美文网首页
[React] How to trigger global co

[React] How to trigger global co

作者: 凸大愚若智凸 | 来源:发表于2021-03-13 12:17 被阅读0次
    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.

    相关文章

      网友评论

          本文标题:[React] How to trigger global co

          本文链接:https://www.haomeiwen.com/subject/iucxcltx.html