bertho-zero/react-redux-universal-hot-example

asyncreducer not work if not inject in defer

Closed this issue ยท 17 comments

asyncreducer not work if not inject in defer , the key exist in the store but no store any change after dispatch in client

I'm also facing this issue, the action will be dispatch as seen in Redux Devtool, but the store simply won't be able to change.

I have to perform inject({ reducerFoo }) in both fetch(server side) and defer(client side) in order to make it work.

yes this is the solution becosue the dispatcher not is stored in the store object , need injected in the client for store now after bertho make the change window._ PRELOADED _

Perfect timing, I am facing the same problem today:

I started by creating this decorator, but redial does not allow to call several @provideHooks, so it overwrites the next.

// THIS DOES NOT WORKS
import { provideHooks } from 'redial';

function createInjector( reducers ) {
  return ( { store: { inject } } ) => inject( reducers );
}

export default function injectReducers( reducers ) {
  return provideHooks( {
    fetch: createInjector( reducers ),
    defer: createInjector( reducers )
  } );
}

My second attempt wraps methods if they exist, it seems to work:

// THIS WORKS
import { provideHooks } from 'redial';
import propName from 'redial/lib/propName';

function createInjector( reducers ) {
  return ( { store: { inject } } ) => inject( reducers );
}

function wrapHookMethod( Component, name, reducers ) {
  const hooks = Component[ propName ];

  if ( typeof hooks[ name ] === 'function' ) {
    const originalFn = hooks[ name ];

    hooks[ name ] = locals => {
      locals.store.inject( reducers );
      return originalFn( locals );
    }
  } else {
    hooks[ name ] = createInjector( reducers );
  }
}

export default function injectReducers( reducers ) {
  return Component => {
    if ( !Component[ propName ] ) {
      return provideHooks( {
        fetch: createInjector( reducers ),
        defer: createInjector( reducers )
      } )( Component );
    }

    wrapHookMethod( Component, 'fetch', reducers );
    wrapHookMethod( Component, 'defer', reducers );

    return Component;
  }
}

It must be before the @provideHooks, and is used like this:

@injectReducers( {
  // ...reducers
} )

Can you try on your side?

The problem not is the connect is becouse the reducer function is anonymous for the dispatcher

Or you can add a hook called inject, and add a trigger redial call before the other 2.

@provideHooks({
  inject: ({ store }) => store.inject( reducers ),
  // fetch
  // defer
})

@joacub I have a different version of https://github.com/bertho-zero/react-redux-universal-hot-example/blob/master/src/components/ReduxAsyncConnect/ReduxAsyncConnect.js, a version that does not use the deprecated method componentWillReceiveProps, that was where the problem was.

I do not have any problem anymore.

i ha ve a version without componentWillReceiveProps but the error still, the error es after the client check PRELOADED, and not make the call to fetch, this si normal and i think not is a bug, i think only need mention this in the documentation so you need put the injecter in the client and in the server calls, becouse the reducer is not present in the clien if is only injected in the server

this is my ReduxAsyncConnect

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter, Route } from 'react-router';
import { trigger } from 'redial';
import NProgress from 'nprogress';
import asyncMatchRoutes from 'utils/asyncMatchRoutes';

@withRouter
export default class ReduxAsyncConnect extends Component {
  static propTypes = {
    children: PropTypes.node.isRequired,
    history: PropTypes.objectOf(PropTypes.any).isRequired,
    location: PropTypes.objectOf(PropTypes.any).isRequired,
    routes: PropTypes.arrayOf(PropTypes.any).isRequired,
    store: PropTypes.objectOf(PropTypes.any).isRequired,
    helpers: PropTypes.objectOf(PropTypes.any).isRequired
  };

  constructor(props) {
    super(props);
    this.state = {
      previousLocation: null,
      location: props.location
    };
  }

  componentDidMount() {
    NProgress.configure({ trickleSpeed: 200 });
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    if (__CLIENT__) {
      const navigated = nextProps.location !== prevState.location;
      if (navigated) {
        NProgress.start();
        return {
          previousLocation: prevState.location,
          location: nextProps.location
        };
      }
    }

    return {
      location: nextProps.location
    };
  }

  async componentDidUpdate(prevProps) {
    const {
      history, location, routes, store, helpers
    } = this.props;

    const { previousLocation } = this.state;

    if (previousLocation && prevProps.location !== location) {
      // load data while the old screen remains
      const { components, match, params } = await asyncMatchRoutes(routes, location.pathname);

      const triggerLocals = {
        ...helpers,
        store,
        match,
        params,
        history,
        location
      };

      await trigger('fetch', components, triggerLocals);
      await trigger('defer', components, triggerLocals);

      // eslint-disable-next-line
      this.setState({ previousLocation: null });
      NProgress.done();
    }
  }

  // async componentWillReceiveProps(nextProps) {
  //   console.log('will');
  //   const {
  //     history, location, routes, store, helpers
  //   } = this.props;
  //   const {
  //     location: { pathname, search }
  //   } = nextProps;
  //   const navigated = `${pathname}${search}` !== `${location.pathname}${location.search}`;

  //   if (navigated) {
  //     // save the location so we can render the old screen
  //     NProgress.start();
  //     this.setState({ previousLocation: location });

  //     // load data while the old screen remains
  //     const { components, match, params } = await asyncMatchRoutes(routes, nextProps.location.pathname);
  //     const triggerLocals = {
  //       ...helpers,
  //       store,
  //       match,
  //       params,
  //       history,
  //       location: nextProps.location
  //     };

  //     await trigger('fetch', components, triggerLocals);
  //     if (__CLIENT__) {
  //       await trigger('defer', components, triggerLocals);
  //     }

  //     // clear previousLocation so the next screen renders
  //     this.setState({ previousLocation: null });
  //     NProgress.done();
  //   }
  // }

  render() {
    const { children, location } = this.props;
    const { previousLocation } = this.state;

    // use a controlled <Route> to trick all descendants into
    // rendering the old location
    return <Route location={previousLocation || location} render={() => children} />;
  }
}

and i think i can quit the setState in didupdate but i dont sure

your ReduxAsyncConnect have decprecate function yet ??

I renamed it RouterTrigger, it needs a trigger props:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter, Route } from 'react-router';

@withRouter
class RouterTrigger extends Component {
  static propTypes = {
    children: PropTypes.node.isRequired,
    history: PropTypes.objectOf( PropTypes.any ).isRequired,
    location: PropTypes.objectOf( PropTypes.any ).isRequired,
    trigger: PropTypes.func.isRequired
  };

  static defaultProps = {
    trigger: () => {}
  };

  state = {
    needTrigger: false,
    location: null,
    previousLocation: null
  };

  static getDerivedStateFromProps( props, state ) {
    const { location } = state;

    const {
      location: { pathname, search }
    } = props;

    const navigated = !location || `${pathname}${search}` !== `${location.pathname}${location.search}`;

    if ( navigated ) {
      return {
        needTrigger: true,
        location: props.location,
        previousLocation: location || props.location
      };
    }

    return null;
  }

  trigger = () => {
    const { trigger, location } = this.props;
    const { needTrigger } = this.state;

    if ( needTrigger ) {
      this.setState( { needTrigger: false }, () => {
        trigger( location.pathname )
          .catch( err => console.log( 'Failure in RouterTrigger:', err ) )
          .then( () => {
            // clear previousLocation so the next screen renders
            this.setState( { previousLocation: null } );
          } );
      } );
    }
  }

  componentDidMount() {
    this.trigger();
  }

  componentDidUpdate( prevProps, prevState ) {
    this.trigger();
  }

  shouldComponentUpdate( nextProps, nextState ) {
    return nextState.previousLocation !== this.state.previousLocation;
  }

  render() {
    const { children, location } = this.props;
    const { previousLocation } = this.state;

    // use a controlled <Route> to trick all descendants into
    // rendering the old location
    return <Route location={previousLocation || location} render={() => children} />;
  }
}

export default RouterTrigger;

In my project I chose to add an inject hook and a trigger call:

await trigger( 'inject', components, triggerLocals );

The decorator @injectReducers became useless, I deleted it.

You think RouterTrigered is better to my ReduxAsyncRouter? Is the same beharvios I see aparently more process in router trigger

@bertho-zero in this project i dont see when you are using RouterTrigger ?

RouterTrigger is not used in this project, I use it at my job for a short time.

Mine is running at componentDidMount and componentDidUpdate, which allows ReactDOM.render to be done directly, RouterTrigger does the rest. There is a shouldComponentUpdate for optimization. It can also be used in all projects that use react-router because the trigger function comes from the props.

I implement in my own project and aperently is good solution y replace the reduz old component with this and in the client create a anonymous function trigger with a code before trigger start first call triggers

const _trigger = async _location => {
      const { components, match, params } = await asyncMatchRoutes(_routes, _location);
      const triggerLocals = {
        ...providers,
        store,
        match,
        params,
        history,
        location: history.location
      };

      // Don't fetch data for initial route, server has already done the work:
      if (window.__PRELOADED__) {
      // Delete initial data so that subsequent data fetches can occur:
        delete window.__PRELOADED__;
      } else {
      // Fetch mandatory data dependencies for 2nd route change onwards:
        await trigger('fetch', components, triggerLocals);
      }
      await trigger('defer', components, triggerLocals);
    };

    ReactDOM.hydrate(
      hot(module)(
        <div id="root">
          <Provider store={store} pageContext={pageContext} {...providers}>
            <Router history={history}>
              <RouterTrigger trigger={_trigger}>
                {renderRoutes(_routes)}
              </RouterTrigger>
            </Router>
          </Provider>
        </div>
      ),
      dest
    );

Something like this