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

Use context across different components #4517

Closed
SMJ93 opened this issue Dec 28, 2018 · 22 comments
Closed

Use context across different components #4517

SMJ93 opened this issue Dec 28, 2018 · 22 comments

Comments

@SMJ93
Copy link

SMJ93 commented Dec 28, 2018

Issue Description

Great work with adding support for React context API @guyca!

I've managed to get context working as expected for individual components:

export default () => {
  registerScreen(routes.welcome, Welcome)
  registerScreen(routes.home, Home)
  registerScreen(routes.settings, Settings)
  registerScreenWithAlertsProvider(routes.game, Game)
}

const registerScreen = (route: string, Screen: any) => {
  Navigation.registerComponent(route, () => Screen)
}

const registerScreenWithAlertsProvider = (route: string, Screen: any) => {
  Navigation.registerComponent(
    route,
    () => (props: any): any => (
      <AlertsProvider>
        <Screen {...props}/>
      </AlertsProvider>
    ),
    () => Screen)
}

Is there a way we can share the context across components / screens?

Steps to Reproduce / Code Snippets / Screenshots

If I update the snippet above to:

  registerScreenWithAlertsProvider(routes.settings, Settings)
  registerScreenWithAlertsProvider(routes.game, Game)

I would expect the Alerts context to be the same on each screen, but it creates a new context for each screen.

  • If I queue an alert on the settings page: alerts: [ {alert: 1} ]
  • then I navigate to the game page it creates a new context with no alerts: alerts: []
  • then if I go back to the settings page it shows the alerts alerts: [ {alert: 1} ]

I would like the context alerts: [ {alert: 1} ] available on all screens.


Environment

  • React Native Navigation version: 2.2.5
  • React Native version: "npm:@brunolemos/react-native" (RN 0.57.7 with hooks enabled)
  • Platform(s) (iOS, Android, or both?): both
  • Device info (Simulator/Device? OS version? Debug/Release?): simulator debug
@guyca
Copy link
Collaborator

guyca commented Dec 30, 2018

@SMJ93 When a component is used in JSX: <AlertsProvider>, it essentially invokes its constructor.
Have you tried instantiating the provider once (for example in global scope), and reusing that same instance across registered screens?

@SMJ93
Copy link
Author

SMJ93 commented Jan 1, 2019

@guyca I see, and usually (with web and other nav libraries) we wrap the provider around the entire app once, but with RNN we wrap it around each screen which invokes the constructor multiple times?

No I've not tried that, do you have any good links / examples showing how to do that? I've not done it before.

@guyca guyca closed this as completed in dd41d48 Jan 8, 2019
@SMJ93
Copy link
Author

SMJ93 commented Jan 20, 2019

Thanks @guyca 👍

@johanholm
Copy link

I'm having the same problem, I'm used to wrap the whole application within one or many Context.Provider's so that every subcomponent will have the same provider[s]. In Game and Friends I have a Context.Consumer and the AppStateProvider component is a React component that returns Context.Provider
The problem is the same as SMJ93, they don't share the same state on the different screens.

Im registrering the components like this, but ofcourse they don't share the same Provider so that won't work.

Navigation.registerComponent('navigation.Game', () => (props) => (
    <AppStateProvider>
        <Game {...props} />
    </AppStateProvider>
), () => Game);
Navigation.registerComponent('navigation.Friends', () => (props) => (
    <AppStateProvider>
        <Friends {...props} />
    </AppStateProvider>
), () => Friends);

How did you solve this @SMJ93 ?

@SMJ93
Copy link
Author

SMJ93 commented Jan 21, 2019

As mentioned by @guyca above you need to use something (like Proxy) to store the global provider state.

He has linked an example above to wix playground: dd41d48

I have managed to get it working on iOS, but be wary older versions of Android / iOS may not support this.

@johanholm
Copy link

I've copied the same code as @guyca wrote in the example – and on my iOS simulator its not working very good, the components isen't re-rendering when the state changes but it seems that they do share the state.
And Android isen't working at all but im sure that because its not compatible with Proxy out of the box.

I'm sure this will be improved in the future but for now it seems like I should try implementing Redux instead.

@SMJ93
Copy link
Author

SMJ93 commented Jan 21, 2019

Here are my files with it working:

GlobalContext - works like normal context, but notice we also have stateAwareContext that uses Proxy

import React from 'react'

const defaultState = {
  count: 0
}

const stateAwareContext = (component: any) =>
  new Proxy(defaultState, {
    set (obj, prop, value) {
      obj[prop] = value

      component.setState({ context: stateAwareContext(component) })

      // Indicate proxy success
      return true
    }
  })

const GlobalContext = React.createContext(defaultState)

class GlobalProvider extends React.Component {
  constructor(props: any) {
    super(props)
    this.state = {
      context: stateAwareContext(this)
    }
  }

  public updateCount = () => {
    const newCount = this.state.context.count + 1
    this.state.context.count = newCount
  }

  public render() {
    const value = {
      count: this.state.context.count,
      updateCount: this.updateCount
    }

    return (
      <GlobalContext.Provider value={value}>
        {this.props.children}
      </GlobalContext.Provider>
    )
  }
}

export { GlobalContext, GlobalProvider }

registerHomeScreen

import React from 'react'
import { Navigation } from 'react-native-navigation'
import { GlobalProvider } from '../../contexts'
import routes from '../../routes'
import { Home } from '../../screens'

export default () => {
  Navigation.registerComponent(
    routes.home,
    () => (props: any): any => (
      <GlobalProvider>
        <Home {...props} />
      </GlobalProvider>
    ),
    () => Home)
}

register settings screen

import React from 'react'
import { Navigation } from 'react-native-navigation'
import { GlobalProvider } from '../../contexts'
import routes from '../../routes'
import { Settings } from '../../screens'

export default () => {
  Navigation.registerComponent(
    routes.settings,
    () => (props: any): any => (
      <GlobalProvider>
        <Settings {...props} />
      </GlobalProvider>
    ),
    () => Settings)
}

Note: I am using hooks

Settings screen

const Settings = (props: Props) => {
  const { count, updateCount } = useContext(GlobalContext)
  console.log('SETTINGS', count, updateCount)

  return(
    <Page testID='screen-settings'>
      <Text>Settings</Text>
      <Container>
        <Button
          testID='screen-home-button-settings'
          label='Test global context'
          onPress={updateCount}
        />
      </Container>
    </Page>
  )
}

Home screen:

const Home = (props: Props) => {
  const { count, updateCount } = useContext(GlobalContext)
  console.log('HOME', count, updateCount)

  return(
    <Page testID='screen-home'>
      <Text>Settings</Text>
      <Container>
        <Button
          testID='screen-home-button-home'
          label='Test global context'
          onPress={updateCount}
        />
      </Container>
    </Page>
  )
}

The count value is the same on both screens.

Hope this helps!

@miwillhite
Copy link

I ended up solving this (for now) by using the new Provider API, but explicitly passing the context around:

// First in the stack (parent scene)
Navigation.registerComponent(
    SCENE.PARENT,
    () => props => (
      <Provider>
        <Parent {...props} />
      </Provider>
    ),
    () => Parent)

// Next in the stack (child scene, in my case a modal)
Navigation.registerComponent(
    SCENE.CHILD,
    () => props => (
      <Provider value={props.ctx}>
        <Child {...props} />
      </Provider>
    ),
    () => Child)

// Then from the parent
Navigation.showOverlay({
  component: SCENES.CHILD,
  options: { ... },
  passProps: { ctx }, // This being the context from the parent: useContext(Provider)
});

I'm not sure I'm going to like this if I have to repeat a lot, but for this application I don't have very deep stacks. I don't mind setting up the new provider instance for each screen.

@oxy88
Copy link

oxy88 commented Apr 29, 2019

Reopen please? This is still a problem

@SMJ93
Copy link
Author

SMJ93 commented Aug 15, 2019

Hey @guyca, I've just updated my project to RN 0.60 and enabled hermes, but the Proxy solution no longer works due to hermes not supporting Proxy:

facebook/hermes#28

Does react-native-navigation have a proper solution to the context API yet?

@jasondenney
Copy link

jasondenney commented Aug 16, 2019

I was directed to this solution by @sijad on Discord#need-help.
@SMJ93 solution above worked great when navigating from screen to screen in sharing the context. However I had a scenario where I would do a showModal for authentication in the case of having optional authentication in my app and then I upon dismissModal after either cancelling and/or completing the authentication the currently loaded screens could reflect this global context change. So currently using bottomTabs in which all the screens for the tabs are loaded and will now respond to the single update without having to passProps of the update method of the screen that triggered the action as that was still not working when I would change tabs to one of the other screens as they still had not received the new context value.

In doing this change I was able to get the update method from either a GlobalContext.Consumer or a useContext(GlobalContext) hook and get all loaded screens to reflect the change.

import React from 'react';
import shortid from 'shortid';

declare type GlobalProviderProps = {
    count: number,
    updateCount: Function
};

declare type GlobalProviderStateType = {
    context: GlobalProviderProps
};

const updateCount = (): void => {
    globalStateValue.count = globalStateValue.count + 1;
    console.log(globalStateValue, GlobalContextProviderInstances);
    for (let instanceId in GlobalContextProviderInstances) {
        GlobalContextProviderInstances[instanceId].forceUpdate();
    }
};

const defaultState: GlobalProviderProps = {
    count: 0,
    updateCount: updateCount
};
let GlobalContextProviderInstances: any = {};

const stateAwareContext = (component: any): any =>
    new Proxy(defaultState, {
        set(obj: any, prop: any, value: any): boolean {
            obj[prop] = value;

            component.setState({ context: stateAwareContext(component) });

            // Indicate proxy success
            return true;
        }
    });

const globalStateValue: GlobalProviderProps = defaultState;

const GlobalContext = React.createContext(defaultState);

class GlobalProvider extends React.Component<GlobalProviderProps, GlobalProviderStateType> {
    private _instanceId: string = shortid.generate();

    constructor(props: GlobalProviderProps) {
        super(props);
        this.state = {
            context: stateAwareContext(this)
        };
    }

    shouldComponentUpdate(): boolean { return true; }

    componentDidMount(): void {
        GlobalContextProviderInstances[this._instanceId] = this;
    }

    componentWillUnmount(): void {
        delete GlobalContextProviderInstances[this._instanceId];
    }

    public render(): JSX.Element {
        const value = {
            count: this.state.context.count,
            updateCount: updateCount
        };

        return (
            <GlobalContext.Provider value={value}>
                {this.props.children}
            </GlobalContext.Provider>
        );
    }
}

export { GlobalContext, GlobalProvider };

@SMJ93
Copy link
Author

SMJ93 commented Aug 18, 2019

@jasondenney Glad to hear it helped you! There is a problem with this solution as it uses Proxy which is not supported by Hermes in react-native 0.60` :(

@EJohnF
Copy link

EJohnF commented Aug 21, 2019

Yea, and this problem is a kind of break deal for the decision to use this library or give up and use react-navigation.

@SMJ93
Copy link
Author

SMJ93 commented Aug 21, 2019

@EJohnF yup, we are going to give it a few weeks, hopefully there is some sort of support or workaround for React Context with RN 0.60 otherwise we are going to have to change to react-navigation 😟

@guyca Are there any plans to support global context?

@EJohnF
Copy link

EJohnF commented Aug 21, 2019

I don't see it in backlog :(

@guyca
Copy link
Collaborator

guyca commented Aug 21, 2019

Hey @EJohnF @SMJ93
I removed Proxy from the playground app. I don't think this is an ideal solution (previous one wasn't ideal as well) but in all honesty, perhaps Context should not be used to share data between screens.

From the React docs:

Context is designed to share data that can be considered “global” for a tree of React components

I'm not sure what was the motivation for making Context only support trees of react components, and not... any react components. Even if they don't share the same root.

@SMJ93
Copy link
Author

SMJ93 commented Aug 21, 2019

Thanks for the explanation @guyca. Yeah I guess that is where React Native Navigation won't work as each screen has a different tree, compared React Navigation which is all one tree 😕

I will try to find some time to see if there are any other workarounds as I was really hoping to avoid moving to React Navigation as this libraries feels so much smoother than react-navigation!

I guess another option is to use Redux, but I was hoping to avoid that!

@sijad
Copy link

sijad commented Nov 21, 2019

can you please give https://github.com/atlassian/react-sweet-state a try?

@chrise86
Copy link

If that's the route you want to take there is also https://github.com/CharlesStover/reactn

@itsam
Copy link
Contributor

itsam commented Dec 16, 2019

So, does this mean we should go for redux?

vshkl pushed a commit to vshkl/react-native-navigation that referenced this issue Feb 5, 2020
Played around with context api, tried to get the wrapped component to rerender after global context
was updated - this is what I came up with.

Note: this commit makes the playground app use a newer js core as the js core which shipped with RN on Android
is missing javascript Proxy.

Closes wix#4517
@ninjz
Copy link

ninjz commented Apr 29, 2020

@sijad 's suggestion is great. I'm having a great experience with that package so far.

@byteab
Copy link

byteab commented Nov 11, 2021

you can use these libraries to tackle React Navigation limitaions.
way better and more performant than Context Api and Zustand.

https://github.com/pmndrs/jotai
https://github.com/pmndrs/zustand
https://github.com/pmndrs/valtio

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