Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature-request] Expose custom context to hooks #1304

Closed
ryaninvents opened this issue Jun 10, 2019 · 13 comments
Closed

[feature-request] Expose custom context to hooks #1304

ryaninvents opened this issue Jun 10, 2019 · 13 comments

Comments

@ryaninvents
Copy link
Contributor

What is the current behavior?

The Provider component allows consumers to pass a custom context, but the current hooks API does not allow that custom context to be used.

What is the expected behavior?

I'd like to be able to access a custom context via React Redux hooks. I'd expect this would be implemented with a "hook factory" pattern for performance reasons, but that's an implementation detail.

Which versions of React, ReactDOM/React Native, Redux, and React Redux are you using? Which browser and OS are affected by this issue? Did this work in previous versions of React Redux?

This issue is unique to the v7-hooks branch. I suppose you could say this has never worked in previous versions of React Redux, since hook support is new 🙂or, it might be argued that the HOC pattern was able to access alternate contexts, so hook support would be incomplete without it.

Note: I have a PR prepared; I didn't read the contributing guidelines until after I'd written the code, which is why I'm doubling back and creating the discussion issue now.

@timdorr
Copy link
Member

timdorr commented Jun 11, 2019

What API are you envisioning for this? I think the factory approach would work, but it would be helpful to see some code.

@ryaninvents
Copy link
Contributor Author

I've got a branch with the following as a proposal:

createCustomHooks()

This utility function returns a set of hooks bound to a different context. You might use this to help manage a very complex reusable component, allowing it to coexist within a Redux application using its own store instance without colliding.

import React from 'react'
import { createCustomHooks, Provider } from 'react-redux'
import createComponentStore, { increment } from './component-store'

const customContext = React.createContext()
const customStore = createComponentStore()

const customHooks = createCustomHooks(customContext)

const Increment = () => {
  const dispatch = customHooks.useDispatch()
  return (
    <button onClick={() => dispatch(increment())}>+1</button>
  )
}

const InternalCount = () => {
  const count = customHooks.useSelector(s => s.count)
  return (
    <span>{String(count)}</span>
  )
}

export const ReusableCounterComponent = ({ children }) => {
  return (
    <Provider context={customContext} store={customStore}>
      <div>
        <Increment /> <InternalCount />
        {children}
      </div>
    </Provider>
  )
}

Note that consumers of your component will want any children to use the top-level application Store, not your Store. By creating a separate context, you will permit nested components to work as expected, instead of attaching to your component's store.

createContextValue

This utility function allows you to use a singleton instance of your Redux store. This is useful in cases where your component will be reusable, but you want all instances to share the same store. For instance, this would be a great way to implement caching without requiring consumers of the component to wrap their application in a top-level <Provider>.

Note that if you create a context using this utility, you will only be able to access it through hooks created with createCustomHooks.

import { createCustomHooks, createContextValue } from 'react-redux'
import store from './store'

const customContext = React.createContext(createContextValue(store))
const customHooks = createCustomHooks(() => React.useContext(customContext))

export const Increment = () => {
  const dispatch = customHooks.useDispatch()
  return (
    <button onClick={() => dispatch(increment())}>+1</button>
  )
}

This is just one way we could expose it; my proposal branch is mostly clean code but there's still room for improvement.

The key pieces are Subscription and useSelectorWithStoreAndSubscription (factored out from useSelector), so even if the only outcome of this request were to expose those two as exports, it would still permit custom contexts.

@timdorr
Copy link
Member

timdorr commented Jun 11, 2019

I think a 1:1 create* function mapping might be better: createUseSelector, createUseDispatch, etc etc. That maps better to the composable nature of Hooks, IMHO.

I'm not really groking createContextValue. You have to render the Provider from the context, otherwise the values don't get applied properly. Relying on the default value of the context isn't correct, as the store could change. You're essentially relying on a singleton store, which is not how all apps are architected.

@ryaninvents
Copy link
Contributor Author

Good point! I was trying to avoid making assumptions about the consumer's application, but ended up assuming all use cases would look like mine instead. 😅

I'll put up a PR within the next few days using the create* API you've proposed.

@ryaninvents ryaninvents changed the title [feature-request; v7-hooks] Expose custom context to hooks [feature-request] Expose custom context to hooks Jun 11, 2019
@markerikson
Copy link
Contributor

Out of curiosity, what's your use case for using a custom context instance rather than the default?

@timdorr
Copy link
Member

timdorr commented Jun 11, 2019

Folks with multiple stores and an interleaved tree, I would assume.

@gnoff
Copy link
Contributor

gnoff commented Jun 12, 2019

I think when i was trying to get tests to all pass on #1276 i ended up with each hook having a corresponding factory 1:1

@ryaninvents
Copy link
Contributor Author

@markerikson This would enable complex reusable components that can be published to npm. @timdorr hit the nail on the head. Consider a reusable <FriendList /> component that allows you to specify how user avatars are rendered:

() => (
  <FriendList
    renderAvatar={(userId) => <Avatar userId={userId} />}
  />
)

Suppose Avatar is a connected component using a service to resolve a userId to profile photo URL. If FriendList has a Redux store on the default context, then Avatar will try to pull the relevant data from the FriendList store and most likely fail. I've accidentally implemented Redux from scratch once or twice (Redux Lite?) to do this type of thing. The Redux ecosystem is so helpful (devtools, thunk middleware, etc.) that I found myself missing it.

@markerikson
Copy link
Contributor

I'm more asking what your specific use case is for multiple stores :) Why would FriendList have a separate store to begin with?

@ryaninvents
Copy link
Contributor Author

ryaninvents commented Jun 12, 2019

In our case, we're going to publish our components, and they'll be used in apps which may or may not themselves use Redux. My goal is to make the component behave as if it were not using Redux at all -- I'd prefer to avoid telling consumers "install these 3 reducers, make sure redux-thunk middleware is in place, ..."

In hindsight, FriendList is a poor example since it's simple. Our actual use case is a complex editor component which allows custom rendering for some of the elements.

I completely understand if this request doesn't fit with the goals of this project and isn't implemented as described 🙂 I'd be okay with factoring out and exposing useSelectorWithStoreAndSubscription; the remaining wrapper code is small enough that I'd feel comfortable maintaining alternate context handling as a separate project.

@markerikson
Copy link
Contributor

I don't actually have a problem with the idea of a way to use a custom context with the hooks. After all, my initial API suggestion somewhere in the discussion threads was along the lines of useSelector(selector, customContext)

I'm just trying to get a better understanding for where and why you feel you actually need the custom context yourself.

I'm still not quite sure I follow the use case you're describing. Are you saying the components use a Redux store internally, and need to read from that as opposed to an app-wide store?

@ryaninvents
Copy link
Contributor Author

Are you saying the components use a Redux store internally, and need to read from that as opposed to an app-wide store?

Yes, that's exactly correct. Sorry for my lack of clarity 😅

I wouldn't mind useSelector(selector, customContext) as an API; I want to implement this in a way that won't create extra work for you all as a result, which was why I started thinking along the current lines. Happy to help in whatever way I can.

@heygrady
Copy link

heygrady commented Jun 22, 2019

We’ve been using react-redux with a custom store where parts of the tree have stateful needs that do not need shared with the whole app. In particular it’s useful where the tree might be repeated.

We had a complex entity viewer with multiple nested accordion sections. It was helpful to rely on react’s useReducer and pass a custom store into react-redux to share that state with the viewer tree.

Because the viewer component could be repeated, keeping that structure in the global redux store was more trouble than it was worth.

@timdorr timdorr closed this as completed Aug 1, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants