当前位置: 首页>后端>正文

[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 backObviously we need some way to pass our callback to UserPicker. Use "global" getter/setter againMaybe, 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.


https://www.xamrdz.com/backend/3yc1996012.html

相关文章: