Search code examples
reduxflutter-redux

Redux how to update computed state


I'm learning redux (in Flutter using Firestore, but I don't think that matters), trying to get beyond the basics, and I'm confused about where 'computed state' (not even sure what to call it) should go.

Say I have app state like this:

  • user: an instance of user
  • movies: a list of user's movies
  • recentFavoriteMovie: one of the movies above that user has marked "favorite" and has the most recent creation date

I'm able to set user (login success action) and request user's movies (login success middleware). When the movies query completes, I'm confused about where to initialize recentFavoriteMovie. There seems to be many choices....

  1. SetMovies middleware can compute it, and then call a SetRecentFavorite action.
  2. Can the SetMovies reducer do it? Or is that considered a side-effect of the reducer, which is not allowed?
  3. I'd like to do it lazily. Is it considered okay in redux to give the app state object a method that computes and caches it? If so, I'd still need to clear the cached value when a new movie list is set. But that seems like the same problem as (b) above.
  4. I could put the movies property and favoriteMovies (property or method) in my user object (they kinda belong there), and then just dispatch an UpdateUser action each time one or the other changes. In general, I don't know when/whether to "promote" some sub attribute of my app state to the top level so the app can react to it.

Are all or any of these valid choices? I hope this makes sense as a question. I might even be too behind the curve to ask this properly.


Solution

  • You almost ther with computed state. From documentation

    Reselect provides a function createSelector for creating memoized selectors. createSelector takes an array of input-selectors and a transform function as its arguments. If the Redux state tree is changed in a way that causes the value of an input-selector to change, the selector will call its transform function with the values of the input-selectors as arguments and return the result. If the values of the input-selectors are the same as the previous call to the selector, it will return the previously computed value instead of calling the transform function.

    That is essentially what yu want with lazy selection of movies.

    In state you store user and movies. Some movies marked as favorites for specific user (so when user marks movie as favorite you only modify movies and not re-run selector).

    When some component needs list of favorite movies, it calls selector which computes derived state (list of favorite movies) and returns it. Also selector will memorize results and recompute them only when store changes but not on each render.

    This approach can be considered best practice as you may later implement some filters for movies list and selectors will help to extract filtered list of movies.

    When using selector you don't required to store selected data (list of favorite movies) in you store.

    Computed state is used in mapStateToPros for each component that require computed state like so

    const makeMapStateToProps = () => {
        const getFavoriteMovies = makeGetFavoriteMovies();
        const mapStateToProps = (state) => (
        {
            favoriteMovies: getFavoriteMovies(state.user, state.movies),
            movies: state.movies,
            user: state.user
        });
        return mapStateToProps;
    }
    

    And makeGetFavoriteMovies may look like

    const getFavoriteMovies = (state) => state.movies;
    const getUser = (state) => state.user;
    
    export function makeGetFavoriteMovies () {
        return createSelector(
            [getFavoriteMovies, getUser],
            (favoriteMovies, user) => {
                // Here can be complicated logic to select only favorite movies
                return movies.filter (movie => movie.isFavorite); 
            }
        );
    )
    

    Reducer and/or middleware can compute favorite movies list. But this is not their responsibility. So to better separate concerns its better to use selectors for this task.

    Also there is not reason for specific middleware (ogin success middleware). You may implement logic in actions and reducer.