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

Native global React context out of route #237

Closed
jacobp100 opened this issue May 7, 2019 · 24 comments · Fixed by #245
Closed

Native global React context out of route #237

jacobp100 opened this issue May 7, 2019 · 24 comments · Fixed by #245

Comments

@jacobp100
Copy link

If I have some global context I want to apply to all routes (login data etc.), what's the best way to do this?

@grahammendick
Copy link
Owner

Use React Context to track your global values and then wrap a Context Provider around your exported App.js component. Using the Theme example from the React docs

export default ({crumb}) => (
 <ThemeContext.Provider value="dark">
   <NavigationHandler stateNavigator={stateNavigator}>
     <Scene crumb={crumb} />
   </NavigationHandler>
 </ThemeContext.Provider>
);

@jacobp100
Copy link
Author

Hmm. I'm looking for something more like,

export default ({ crumb }) => (
  const [state, dispatch] = React.useDispatcher()
  
  return (
   <Context.Provider value={{ state, dispatch }}>
     <NavigationHandler stateNavigator={stateNavigator}>
       <Scene crumb={crumb} />
     </NavigationHandler>
   </Context.Provider>
  )
)

But importantly, we only initialise the reducer once.

If I understand correctly, we re-initialize the app, so I'd lose the context.

This might be less of an issue for people using Redux where there's a singleton store, but it'd be nice to use the native React APIs.

@grahammendick
Copy link
Owner

grahammendick commented May 7, 2019

That works. You don't need Redux but you do need a singleton outside of the component tree so that you can use the values across routes.

// Global so it can be used across routes
var store = {};

export default ({ crumb }) => (
  // Take the initial value from the global store
  const [state, dispatch] = React.useReducer(, { ...store } )
  
  // Update the global store with the latest state
  React.useEffect(() => {
    store = { ...state };
  });

  return (
   <Context.Provider value={{ state, dispatch }}>
     <NavigationHandler stateNavigator={stateNavigator}>
       <Scene crumb={crumb} />
     </NavigationHandler>
   </Context.Provider>
  )
)```

@jacobp100
Copy link
Author

I think that'll have a few issues with updating state across multiple routes.

It was definitely possible to do this in the (now removed) NavigatorIOS. It might be possible to do it in this library, but you might find it out of scope, or there might be tradeofs.

It internally mounted every route as a React child, and then used the {insert,remove}ReactSubview callbacks to navigate. This was good so you got full access to the React tree.

But it had really odd logic to sync the JS and iOS view state, including setTimeouts. There were definitely bugs relating to animations and back gestures. But maybe your JS-driven only approach combined with their React children approach could be a neat solution

@grahammendick
Copy link
Owner

The Navigation router uses this approach. It has a global stateNavigator that gets passed into every route. What issues do you mean?

@jacobp100
Copy link
Author

When the state in one route updates, it’ll have to notify all other routes to update their states. But once you’ve done that you might as well just use redux

@grahammendick
Copy link
Owner

But login data only updates once so you don't need any notifications

@jacobp100
Copy link
Author

It was just an example. I’m making a music app, and stuff like the current song, playlist etc is shared between routes

@jacobp100
Copy link
Author

I’ve got some ideas on what this might look like. I’ll try and find some time to play around with it in a new repo and paste it here. I reckon your render function in iOS combined with some of what they did in NavigatorIOS could work really well

@grahammendick
Copy link
Owner

Cool, I look forward to seeing your ideas

@grahammendick
Copy link
Owner

Why don't you like a Redux-like solution?

@jacobp100
Copy link
Author

Okay, I have a demo!

https://github.com/jacobp100/NavigatorTest

I wanted to go really React-like with this, so there's no imperative navigations. You just pass in the routes you wanna render, and it'll do it.

<Navigator
  style={{ flex: 1 }}
  routes={routes}
  onRoutesUpdated={onRoutesUpdated}
/>

I know there's definitely some issues with the animations between routes - the titles seem to flash on mounting a route rather than slide in. But the back gesture is pretty smooth!

@grahammendick
Copy link
Owner

grahammendick commented May 9, 2019

Wow, thanks for that. It’s fascinating. So that’s how NavigatorIOS used to work then, is it? I can foresee a few problems

  • What about Android? Activities aren’t as easy to manipulate as UIViewControllers. Perhaps you could try the single Activity approach and work with fragments instead, like I think React Native Navigation does?
  • What about UITabViewController? Could you extend this approach for tabs on iOS without sacrificing the single React tree?
  • What about the back gesture? The back gesture happens on the native client but you’re managing the stack in a single tree. So you’d have to trigger a setState after the gesture completes to remove the route from the tree. But what if there are two back gestures in quick succession? During the second gesture the first setState completes and puts the scene back!

I guess problems like these brought about the downfall of NavigatorIOS.

@jacobp100
Copy link
Author

I haven’t thought about android too much — I’ve never actually done any native development with it. Is there advantages to using fragments over regular views and adding a toolbar? Failing that, I imagine I could just work out what pushes and pops are needed. Any tips would definitely be welcome here!

There’s actually TabBarIOS in the React native core. This one is really solid, and already has access to the React tree

Yeah there is a setState needed — I did onRoutesChanged. This bit needs a bit more work though, if they don’t call setState it should really push the old view back after the gesture (like how <Switch>es behave)

@grahammendick
Copy link
Owner

grahammendick commented May 10, 2019

You should definitely keep going with your Navigator. I'm excited to see how it progresses.

You should try getting it to work with Activities on Android. If you have trouble then take a look at the NavigationComponent

The TabBarIOS component doesn't render a UITabViewController. You'll probably need to render multiple Navigators, one for each tab

<Navigator routes={tab1routes} />
<Navigator routes={tab2routes} />
<Navigator routes={tab3routes} />

When you're implementing popping, try swiping back twice in quick succession. I think it will cause you problems because the native stack and JavaScript stack will be battling with each other

@jacobp100
Copy link
Author

Yeah, I'll definitely check out how you did Android!

You'll probably need to render multiple Navigators, one for each

Yeah, that's what I did before when NavigatorIOS existed. Also, some apps go the other way round, where the tab bar is in the route, because only some of the routes have tab bars (like Apple News)

I know there will definitely be some race conditions over navigating. I guess in theory the JS could push a route while the user is doing the back gesture. Gotta think about this one!

@grahammendick
Copy link
Owner

For clarity, the NavigationComponent isn't mine. It's part of Android. It's the in-built way to navigate with Fragments instead of Activities

@grahammendick
Copy link
Owner

grahammendick commented May 10, 2019

I’ve created a Redux example for React Native. It’s a master/details scenario where you can select a person from a list and edit their name. The Blinkers component ensures that Redux only propagates store changes to visible scenes. Put blinkers on Redux so it can only see what's directly in front of it, the visible scenes. So the edit to a person's name doesn't propagate to the list of people until you pop/swipe

@jacobp100
Copy link
Author

👍

Still relies on Redux though 😉

One of the apps I'm rewriting in RN has some pretty crazy context providers that I wouldn't be ergonomic in Redux (web workers, input responder chain for math input fields, parent scroll view references etc.)

The Blinkers stuff is neat! I had no idea you could do that, but that would have been really handy on one project I was on. Eventually we'll be able to de-prioritize parts of the tree in RN like <div hidden> on the web. One day... 😄

@grahammendick
Copy link
Owner

I think you'd need Redux even if it was a single React tree because Context doesn't scale at the moment. React Redux had to back out their reliance on Consumers

Not sure how deprioritzing parts of the tree would play with the swipe back. It's too expensive to trigger a setState just to mark the peeked screen as visible.

You happy to close this issue?

@grahammendick
Copy link
Owner

grahammendick commented Jun 1, 2019

I'm reopening this. I ran into a problem implementing a search bar component that means can't add UISearchController inside a UIView (either because of async React Native or because views always run async).

Changing to a single react root means loading views first and then navigating (instead of other way round with current approach). So should be able to get the search bar and add it in the viewDidLoad of new UIViewController.

The single root approach could address other problems:

  1. Shared state between scenes without having a global store outside of the React tree (this issue)
  2. Don't need any iOS native setup. In @jacobp100's test navigator he made no changes to AppDelegate.m. If can get the Navigation router working without any special native setup then nothing stopping it from working in expo (expo would only need to include navigationpackages)
  3. Switching between UINavigationController and UITabViewController can all be done from React. Currently have to add native code to do this switch (after logging in for example). Likewise, can configure the tabs in React
  4. Remove delay in loading Right/Left bar buttons. Currently the buttons don't show up until the pushed view controller completes animation. Like with the search bar, can add the buttons in viewDidLoad so they'll show up straight away

There are things to worry about with single React root.

  1. The iOS back happens on the client. Will it get out of step with React?
  2. How does this work with Android Activities?

@grahammendick grahammendick reopened this Jun 1, 2019
@jacobp100
Copy link
Author

The iOS back happens on the client. Will it get out of step with React?

We do need some logic on the iOS side to wait for a back animation to complete before rendering the new routes. The real question is how this can be handled in the JS side — what’s the logical thing to do if you push a route onto a stack while a back animation is in progress?

I have no idea if this approach works with Android. But this approach can be implemented purely in JS. What does doing this in the native android side give us?

@grahammendick
Copy link
Owner

grahammendick commented Jun 1, 2019

We can let the iOS back happen without doing a new render. iOS will remove the ViewControllers but we keep the subviews. There's no need to remove them. Then the views still match the React component tree. On the React side, each scene has their own Navigation context, so they're isolated from changes to the stack (from a scene's perspective, it always thinks it's at the top of the stack).

We should use the platform for Android navigation, same as we do for iOS. Why implement the animations in JavaScript when Android already animates between Activities? Another example is shared elements. Android provides shared element transitions when starting a new Activity. It makes sense to use the shared element animations provided by the platform rather than implement our own.

@jacobp100
Copy link
Author

I meant there’s a case where a route is pushed based on some timeout, and they’re doing a back swipe at the same time a new route gets pushed. I’m not sure there’s a straightforward way to reconcile that

Ah last I used android was Jelly Bean. Guess stuff has moved on a long way since. Pretty sure back then there was never an animation for that kind of stuff. I’ll probably have to spend some time doing native android dev at some point

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

Successfully merging a pull request may close this issue.

2 participants