Skip to content
This repository has been archived by the owner on Aug 8, 2024. It is now read-only.

Redux support: what's it for? #14

Closed
brentvatne opened this issue Feb 9, 2018 · 24 comments
Closed

Redux support: what's it for? #14

brentvatne opened this issue Feb 9, 2018 · 24 comments

Comments

@brentvatne
Copy link
Member

brentvatne commented Feb 9, 2018

@satya164 asked about this on Twitter and received a handful of responses, gathered below:

  • How do you cause navigation as a side effect to redux state change? How do you persist navigation state? These are my main use cases for redux integration (plus some edge cases around rewriting navy state history). In an app where networking is done from a saga/epic/thunk you may want to request data and update a loading state before navigating to the next screen. Doing these as two sequential method calls in component feels too coupled. @jevakallio
  • To trigger navigation actions from side effects this approach works great. @hectahertz
  • Easier navigation to nested routes from anywhere. (From actions mostly). @liamhubers
  • I remember using rnav with redux so i can prevent double dispatch of a page if the user clicks fast enough. Ex-nav used to handle this but not rnav. I guess there was other way to prevent this, without redux. @tychota
  • We are about to use Redux with React navigation, because we need some calculations in different screens and we don’t think that should be passed in params.
  • To dispatch actions from sagas. Redirect to an error screen when some fetch fails. @sibelius
  • I use the same StackNavigator in all tabs of a TabNavigator because many of the screens are shared (except the root). The screen don’t have to know where is it. The reducer know which stackrouter to give the action regarding index of the tabrouter. (because React Navigation doesn't make it possible to handle this currently) @Freddy03h
  • Also it’s very convenient to open a modal (in root stacknavigation) with a redux action from anywhere. Don’t open twice the same view on stack. Reinitiate all routers on logout. @Freddy03h
  • Also use redux-saga to handle fetch instead of doing it on componentDidMount. @Freddy03h
    1. To prevent navigating twice to the same route 2. To navigate in sagas 3. To create some custom navigation actions, specific for the app (RESET is a bit finicky) Example: https://github.com/Minishlink/DailyScrum/blob/04c7d19bd4960deea05f63c1d2b7b2e90f9a2d0c/src/modules/navigation/reducer.js @Minishlink
  • It's really nice way to navigate when fetching data is completed (instead of using callback style that passed when dispatching action). Good fit with redux-saga and easier to test. In my case is user login. I mostly put every side effect in redux-saga. Then, after the authentication is succeed, I can simply dispatch an action to navigate into the app. The other way that I tried is using callback, like onSuccess, when dispatching the action. For testing, I don't really test the navigation itself, but integration with redux helps me to fully test the saga that it dispatches the right navigation action with the right data when side-effect happened 😁 @AudyOdi
  • Trigger custom action called INITIAL_ROUTE. If user is not logged in set login, else set to home. @lucianomlima
  • Use case: opening deep links where navigation with both the top level and nested navigators is required.
  • Use case: a feature that needs to been enabled that is preventing some use of the app. Maybe show an alert. Then, going from wherever the user is where they cannot access that feature to wherever in the app they need to go to enable that feature.
  • One nice benefit I got from this integration is responding to notifications. I can easily dispatch an action using the notification’s payload and push a new screen to show context.

Ideally you shouldn't need Redux integration to do these things.

@ericvicenti
Copy link
Contributor

ericvicenti commented Feb 10, 2018

I'd like to sort these bullets into a few rough categories.

Of these categories, I want to flesh out the framework features. The router fixes should be straightforward. And documentation is really about talking to people and making sure these use cases are documented without redux.

Missing framework feature

  • Sagas and async navigation. What are the specific use cases here?
  • Dynamic initial route. Race condition tradeoffs.
  • State persistence
  • Accessing screen state from router, controlling router behavior from screen instance

Routers/Reducers need more features

  • Idempotent navigate
  • Dynamic reconfiguration of tabs

Use case is not documented

  • Confusion about nested routes not working
  • Top level dispatch, for responding to notifications, opening deep links, or triggering actions from side effects
  • Document custom router use cases, such as blocking actions

@lucianomlima
Copy link

What's the plan here? Show a non redux usage for these questions?

@brentvatne
Copy link
Member Author

@lucianomlima - make sure that these use cases are covered and documented by features that don't require redux

@RobIsHere
Copy link

IMHO it's one of the problems of the library, that it tries to be a redux library and at the same time a non-redux lib. The outcome is: it's perfect for none of them.

I'm in favor of redux for its simplicity and testability with minimal effort. That's just my personal preference. So when I control navigation by redux, I can easily test that the navigations get done like the should by just looking at redux. No mocks and all their issues needed. Just some very simple tests. Thats also my saga use-case as a response to above: easiest possible testing.

To take it further: redux has just about 200 LOC, that's nothing. Integrating it in RN is an option. Then you could configure "use user specified redux store", otherwise it uses the internal one. So react-navigation could provide a really mind-blowing interface on redux and would get easier testability for free.

The "without-redux-api" could just be a facade or an adapter that simplifies the full-blown redux interface, using it under the hood. There could also be a expert-level and a normal-level facade.

That separation of concerns of the different apis could really clean a lot of things up, if well done.

@ericvicenti
Copy link
Contributor

The "without-redux-api" could just be a facade or an adapter that simplifies the full-blown redux interface, using it under the hood. There could also be a expert-level and a normal-level facade.

This is very similar to the original design of the library. If you want to use redux, you can provide a navigation prop and get expert-level control of your application. If you want the easy mode (normal-level facade), then you can just render your navigator normally, and createNavigationContainer will take care of things. It doesn't use redux directly, but it doesn't need to, because as you point out redux is very simple. createNavigationContainer is also around 200 lines.

IMHO it's one of the problems of the library, that it tries to be a redux library and at the same time a non-redux lib. The outcome is: it's perfect for none of them.

For reasons you've already described, @RobIsHere, this should be fixable! Why is it imperfect? Lets fix those issues.

We have a new helper library by @Ashoat to help play better with redux. Maybe this helps the redux use case? What else are you struggling with?

@salujaharkirat
Copy link

@brentvatne @ericvicenti thanks for listing down all the issues. I went through redux react-navigation documentation few weeks back and I was wondering why is redux actually required here. My use cases are mostly the one's you listed and as you said I was able to achieve all of them without using redux.

What I am wondering is that is redux required only if I want to gain expert-level control of the app or are there any specific use cases too?

Once again thanks a lot :)

@jamsch
Copy link

jamsch commented Mar 16, 2018

I personally use Redux (& Redux Saga) to handle the navigation state for the following:

  • Tracking visited screens with Redux middleware for analytics
  • Saga to handle a race condition between navigation events that head towards a different route and performing a HTTP request. (i.e. active request is cancelled if user navigates to a specific route)
  • Saga to perform a HTTP request and navigate afterwards in response to something like a log in request
  • AsyncStorage rehydration using redux-persist and populating the navigation state based on user authentication.
  • Preventing navigation to the same screen twice

How would one otherwise handle the navigation state outside of React Components?

@salujaharkirat
Copy link

@jamsch you can refer to react-navigation/react-navigation#1439 (comment) to handle navigation outside the components.

@brentvatne
Copy link
Member Author

can you folks who need redux right now comment on how the following PR would or would not solve your use case, and explain what your use case is in detail there so that we can factor it into api design? thanks!

@lxcid
Copy link

lxcid commented Mar 25, 2018

I don't have much thoughts but I just want to add my voice that I use redux and redux saga with react-navigation as well.

@brentvatne
Copy link
Member Author

@lxcid - what for?

@brentvatne
Copy link
Member Author

why not use redux / redux saga and just access the global navigator object from sagas, rather than dumping your state in redux as well? https://reactnavigation.org/docs/navigating-without-navigation-prop.html

@lxcid
Copy link

lxcid commented Mar 25, 2018

That might be able to work for me, but I also like to be able to handle all my state in a centralised state container like redux. I really like the idea that react-navigation plug so well to redux.

I try to avoid global service like what describe in the article, only do them for things I really have little control like websocket and apollo client. But I hook them up in middleware, which is very similar to how the article does it but I always felt its an hack.

I haven't done it yet, but I think having the option to write reducers or selectors that parse the state tree of redux navigation is also quite appealing to me.

You get to see the big picture of where the state of your navigation is together with all your other state so it help in debugging.

Much of my app is heavily dependant on redux so with everything being an action, so I kinda benefit from the seamless integration at the moment.

I think the cons for you guys is that if we depend on any of the internal state, you will easily break our implementation.

@brentvatne
Copy link
Member Author

indeed, we can break it at any time. additionally, another downside is that the redux implementation is much more difficult to optimize because we can't take care of it for you, you need to do it in user space

@jamsch
Copy link

jamsch commented Mar 25, 2018

Are we able to listen to the navigator's navigate events? I have something like the following which needs to know the current route the user's on while they're interacting with the app.

function* navigateEvent(cb) {
  const navState = yield select(navSelector);
  const currentRouteName = getCurrentRouteName(navState);
  // Checks if the user navigated to a screen that's allowed
  if (validRouteNames.includes(currentRouteName)) {
    yield cb();
  }
}

// Listens for a 'NAVIGATE' navigation event, checks if user navigated to an allowed route
function* navigateAway() {
  yield take(NavigationActions.NAVIGATE);
  yield call(navigateEvent, navigateAway);
}

// Listens for a 'BACK' navigation event, checks if user navigated to an allowed route
function* navigateBack() {
  yield take(NavigationActions.BACK);
  yield call(navigateEvent, navigateBack);
}

// Task get's executed, cancelled when user navigates to some route
function* handleTask(action) {
  const winner = yield race({
    task: call(doSomeHeavyTask, action.taskId),
    navigateAway: call(navigateAway),
    navigateBack: call(navigateBack),
  });

  // Cancel task if winner is either "navigateAway" or "navigateBack"
}

@Freddy03h
Copy link

Hello,

I currently try to use less redux to handle react-navigation in my app.
Before : each navigators was separate and handled by a reducer with separate objects ( appTabs, newsStack, seriesStack, etc).

Now I connected all Navigators and I stil keep redux to not open twice the same view (I don't like the key solution that can go back to a previous route, but it's not what I want to talk about).
I also tried SwitchNavigator inside a Tab to display a view if the user is not connected or a StackNav if connected :

  • TabNavigator
    • SwitchNavigator
      • NotConnectedView
      • StackNavigator

But in my case, the NotConnectedView in SwitchNavigator don't handle the auth action, it only receive a Boolean isUserConnected from redux. And it's not easy to handle the displayed view using focus and navigate. I only want a if … else.

I think my redux use case is more about having a way to rewrite navigation.state between two navigators.

In a route config it can be nice to replace screen: MyStackNavigator by this screen: (navigation, screenProps) => <MyStackNavigator navigation={navigation} screenProps={screenProps} /> so I can re-write the state. But It handle an error because there's no more static router to recreate the global router.

I create a wrapper component around my router to handle it, and end with something like this :

const CollectionStackNavigator = StackNavigator(
  {
    collection: {
      screen: CollectionView,
      path: 'collection',
    },
    ...
    notConnected: {
      screen: NotConnectedView,
      path: 'notConnected',
    },
  },
  {
    initialRouteName: 'collection',
  }
)

class CollectionStackNavigatorWrapper extends React.Component {
  static router = CollectionStackNavigator.router;
  static navigationOptions = null;

  render() {
    const { navigation, screenProps, userConnected } = this.props

    const connectedNavigation = {
      ...navigation,
      state: userConnected
        ? navigation.state
        : CollectionStackNavigator.router.getStateForAction(CollectionStackNavigator.router.getActionForPathAndParams('notConnected'))
    }

    return (
      <CollectionStackNavigator
        navigation={connectedNavigation}
        screenProps={screenProps}
      />
    )
  }
}

function mapStateToProps(state) {
  return {
    userConnected: selectorUserConnected(state),
  }
}

export default connect(mapStateToProps)(CollectionStackNavigatorWrapper)

It work well for this case with an attribut from redux store, but I also use it for other non-redux use case.
Giving parent navigation params to children using screenProps (example : a filter option) and also read a children navigation params value in a parent navigator (example: hide the TabBar for a specific param in a view route in inner stack).

I hope my app use case can help.

@dantman
Copy link

dantman commented Apr 1, 2018

It's a side effect. But it's nice having access to the Redux DevTools in the react-navigation context. After I switched to using redux I started opening up the React Native Debugger and looking at the navigation portion of the redux state to debug things when react-navigation was behaving strangely.

It's thanks to those Redux DevTools and React Navigation's state being in Redux that I easily found out that a misconfiguration in my pretty deep and complex navigator hierarchy was resulting in as sort of memory leak, where old routes were retained but not visible instead of replaced.

@brentvatne
Copy link
Member Author

@dantman that’s the most compelling use case I’ve heard of so far!

@fatfatson
Copy link

I use navigation with redux because I want the UI changes could be traced back as other normal states.
but I found it's difficult to achieve this:
every time I jump to a previse state, it will generate new action automatically
image

@brentvatne
Copy link
Member Author

@fatfatson - sounds like we need to provide a tool that lets you do that without redux!

@hedgepigdaniel
Copy link

I posted also here

Ideally you shouldn't need Redux integration to do these things.

Ideally in a react redux app, you should not need a separate library to maintain its own state separate from redux, which is meant to be the one central store of all state (and doing so makes many things much more difficult).

sounds like we need to provide a tool that lets you do that without redux!

Why reinvent the wheel? redux dev tools works great (including in react-native-debugger). Also, why have two separate streams of event when you could have just one?

@sibelius
Copy link

hooks + context?

@brentvatne
Copy link
Member Author

@hedgepigdaniel - do you store react-router state in redux in a browser? a lot of state is local to components as well. there are countless examples of state that doesn't go into redux. it's tempting to move everything to redux but i just don't think that's a good idea.

Why reinvent the wheel? redux dev tools works great (including in react-native-debugger). Also, why have two separate streams of event when you could have just one?

a more specialized tool can provide more value. it could be made to work with react-native-debugger as well. anyhow i'm not building this right now but if someone wants to try that'd be fun.

anyhow we no longer provide explicit support for storing react-navigation navigator state in redux. you're welcome to continue to do this, but we won't help you with it and we won't test against it. of course you can continue to use redux or whatever you want to manage the other state in your app.

@hedgepigdaniel
Copy link

hedgepigdaniel commented Jan 28, 2019

I don't use react-router - for my production apps I use redux-first-router, which synchronises URLs with redux actions of your choice, as well other things, like providing a simple trigger for side effects of Redux actions. I find that it leads to an app architecture that is much easier to understand and test (basically M/V/C with Redux/React/redux-first-router) than what tends to happen when using react-router (everything crammed into components). In the past I've also used redux-first-router on react-native with react-navigation (v1.0) but recently I've had to do quite a bit of experimentation and research to get react-navigation to render the state I pass to it rather than doing its own thing.

That article I linked in the other thread was a big one for me in terms of keeping state in Redux and moving side effects out of components. I think its a good idea to move any state into Redux that has some chance of being relevant to app logic. If you need to look at some state to answer questions like:

  • Do I need to do an API call to download extra data?
  • Do I need to display a dialog before leaving this screen/page?
  • What are the details of the user who's profile page I am currently viewing?

Then I would argue that that state should be in Redux. If you have state coming into a component from Redux, and separate routing state coming from other HOCs (withNavigation, withRouter, etc). Then you have to have code in the component that can combine that state and project what you need to render (where in a pure redux setup you can have a selector selectAnyThingYouWant(state) totally independent of the component). If you have component state thats related in any way, it gets worse. Now you need to combine all three sources of state on each render. Hello getDerivedStateFromProps and componentDidUpdate. So you've got maybe three separate sources of state, and code in the component to combine them. Then what if you need to perform side effects depending on that combined state, like downloading data from an API? Then you've got to trigger side effects in componentDidMount. Doing it there means you're more likely to leave any extra resulting state in the component instead of lifting it up to Redux where it belongs. Its more likely you'll have duplicated code combining the same types of state in different components, doing the same API calls for slightly different purposes, etc.

The process of state management, side effect handling, and rendering is all done in the component with no clean separation of concerns. Testing components like this is so difficult that I don't think anyone actually does it. You would have to mock out APIs, simulate complex asynchronous timelines, and pass in three different contexts just to find out if a component renders the right thing in the right situation. And that's just for one component, but there's a whole tree of them! If they have global side effects like loading data into redux, they are probably causally connected aswell, so some components might only work when other components are rendered first, etc.

Contrast that to an app where all significant state is stored in Redux (especially routing/navigation state). Side effects like API calls are triggered in response to Redux actions. Components don't process and mix up props from different state containers - instead selectors are used to pass in the state from redux in the shape that is convenient for the component. Suddenly testing is easy. Components don't have state, lifecycle handlers, side effects, etc. They are nothing but a pure function that takes props and renders something. Same with selectors. Its very difficult to get confused. And its easy to see what in your app is making API calls or having side effects. Instead of having to dig through a huge component tree and guess which component had what state and when and why and which lifecycle handler in it decided to do it, All you have to look at is the redux actions in the log and the side effects attached to them. If the UI looks wrong at any time, you can look at the redux state (including its history) easily and get a good idea of why that is the case.

a lot of state is local to components as well. there are countless examples of state that doesn't go into redux. it's tempting to move everything to redux but i just don't think that's a good idea.

Why though? There is state that I think is harmless to put in components - the kind of state that is ephemeral and isolated. Whether a dropdown is open, the state of an animation, etc. In that case you save a bit of typing by keeping in in the component. But really saving some typing is all you gain. If you keep important state with implications to your apps behaviour in multiple places, the consequence is that your entire app is difficult to reason about and debug.

a more specialized tool can provide more value. it could be made to work with react-native-debugger as well. anyhow i'm not building this right now but if someone wants to try that'd be fun.

What value would it provide? What I would like to see in a debugger is the current state. e.g. in a StackNavigator:

  • what is the current stack?
  • What is the history of push/pop etc that made it get to that state?
  • What triggered these actions and why?
  • Perhaps I can even time travel through past states of the app and interactively see how the app renders with a different navigation state or find the specific state where a bug is triggered.

AFAICT, Redux can already provide everything one would want from a specific tool. All that would be accomplished with a separate tool is introducing confusion about which Redux and navigation states happened at the same time (because there would be two logs of interleaved state changes, not just one), and making time travel difficult/impossible for the same reason.

anyhow we no longer provide explicit support for storing react-navigation navigator state in redux. you're welcome to continue to do this, but we won't help you with it and we won't test against it. of course you can continue to use redux or whatever you want to manage the other state in your app.

Sure. I've been working on it and it still seems to be possible, although it requires many leaps of faith and alot of experimentation. I guess I just want to say that I think it would be good to continue to support that simply if its possible, and I'm trying to explain why I think its important. I'm not sure what sort of questions people ask, but I can imagine some possible benefits of good redux integration in terms of maintenance of this library:

  • less logic to support. Instead of helping people with potential state management bugs, only rendering bugs are relevant. The question goes from "What did you do to all the nested navigators in what order" to "What props are you passing to the specific navigator that is not working?"
  • Easier testing. For the same reason apps are easier to test with a good MVC architecture. It's generally healthy IMO to have a separate view layer (components which simply render based on their props and attach interactions to event handler props). That's basically whats needed for redux integration. If it was easy to import a component from react-navigation that skipped all the routers, state management, etc and just rendered the nice tabs, handled swipe gestures or whatever, it would also be easier to write unit tests for the library.
  • Easier debugging. Going out on a limb here because I haven't tried working on react-navigation... but wouldn't it be nice to have a log of state changes in redux? What if the library provided a redux reducer, exported connected components that behave like they currently do, and also the wrapped unconnected components in case the user wants to manage the state differently? Wouldn't that help to find bugs, e.g. to see if the bug was in the state management or rendering component, and see exactly at which state transition it went wrong?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests