carloslfu / xstate-router

XState Router. Add routes to your XState machine.
MIT License
112 stars 16 forks source link

Unable to use `routerMachine` in a functional component #12

Closed ishabo closed 4 years ago

ishabo commented 4 years ago

The routerMachine works with a class component like a charm. Except, in an all-functional components project like mine it fails to run and produces the following infiniate rerending error:

index.js:2178 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render....

Here's my code:

const Enrolment = () => {
  const options = {
    actions: {
      ...
    },
  };
  const initialContext = {};
  const { state, send } = routerMachine({
    config: enrolmentMachineConfig,
    options,
    initialContext,
    history,
  });

  const renderComponent = () => {
    switch (state.value) {
      case 'introduction':
        return <Introduction />;
      case 'emailCheck':
        return <EmailCheck />;
        ...
      default:
        return null;
    }
  };

  return (
    <EnrolmentMachineContextProvider value={state.context}>
      <EnrolmentMachineStateProvider value={state.value}>
        <EnrolmentMachineSendProvider value={send}>
          <>
            <Header />
            {renderComponent()}
            <Footer />
          </>
        </EnrolmentMachineSendProvider>
      </EnrolmentMachineStateProvider>
    </EnrolmentMachineContextProvider>
  );
};
export default Enrolment;

I tried using useService from @xstate/react but that still didn't help, and it seems that's because routerMachine triggers service.onTransition on every render.

The way I solved this problem is by forking your lib, refactoring routerMachine so to reuse its functionality and created a new function: useRouterMachine.

I refactored routerMachine function with the to the following:

export function createRouterMachine<
  TContext = any,
  TState extends StateSchema = any,
  TEvent extends EventObject = any
>({
  config,
  options = ({} as MachineOptions<TContext, TEvent>),
  initialContext = {},
  history = createBrowserHistory(),
}: RouterArgs): StateMachine<TContext, TState, TEvent> {
  const routes = getRoutes(config)
  const enhancedConfig = addRouterEvents(history, config, routes)
  const currentLocation = history.location
  const enhancedContext = {
    ...initialContext,
    match: resolve(routes, currentLocation),
    location: currentLocation,
    history
  }

  return Machine(enhancedConfig, options, enhancedContext);
}

export function routerMachine<
  TContext = any,
  TState extends StateSchema = any,
  TEvent extends EventObject = any
>({
  config,
  options = ({} as MachineOptions<TContext, TEvent>),
  initialContext = {},
  history = createBrowserHistory(),
}: RouterArgs) {
  const machine = createRouterMachine({config, options, initialContext, history})
  const service = interpret(machine)
  service.start()

  handleTransitionEvents(service, history, getRoutes(config))

  return service
}

export function useRouterMachine
<
  TContext = any,
  TState extends StateSchema = any,
  TEvent extends EventObject = any
>({
  config,
  options = ({} as MachineOptions<TContext, TEvent>),
  initialContext = {},
  history = createBrowserHistory(),
}: RouterArgs) {
  const machine = createRouterMachine({config, options, initialContext, history})
  const [state, send, service] = useMachine(machine);

  useEffect(() => {
    handleTransitionEvents(service, history, getRoutes(config))
  }, [])

  return {state, send, service};
}

export function handleTransitionEvents (service, history, routes) {
  let debounceHistoryFlag = false
  let debounceState = false
  handleRouterTransition(history.location)

  service.onTransition(state => {
    const stateNode = getCurrentStateNode(service, state)
    const path = findPathRecursive(stateNode)
    if (debounceState
        // debounce only if no target for event was given e.g. in case of 
        // fetching 'route-changed' events by the user
        && debounceState[1] === path) {
      debounceState = false
      return
    }
    if (!matchURI(path, history.location.pathname)) {
      debounceHistoryFlag = true
      const uri = buildURI(path, state.context.match)
      history.push(uri)
      service.send({ type: routerEvent, dueToStateTransition: true, route: path, service: service })
    }
  })

  history.listen(location => {
    if (debounceHistoryFlag) {
      debounceHistoryFlag = false
      return
    }
    handleRouterTransition(location, true)
  })

  function handleRouterTransition(location, debounceHistory?: boolean) {
    let matchingRoute
    for (const route of routes) {
      const params = matchURI(route[1], location.pathname)
      if (params) {
        matchingRoute = route
        break
      }
    }
    if (matchingRoute) {
      debounceState = matchingRoute[1]  // debounce only for this route
      service.send({ type: routerEvent, dueToStateTransition: false, route: matchingRoute[1], service: service })
      const state = service.state.value
      if (!matchesState(state, matchingRoute[0].join('.'))) {
        const stateNode = getCurrentStateNode(service, service.state)

        if (stateNode.meta && stateNode.meta.path) {
          if (debounceHistory) {
            debounceHistoryFlag = true
          }
          history.replace(stateNode.meta.path)
        }
      }
    }
  }
}

What do you think? Do you think it's worth including this change and renaming the project to xstate-react-router ?

By the way, I noticed you have 15 months old repo called xstate-react-router which has a different type of implementation. I am not sure what you intend to do with that project although it seemed interesting enough to have a component wrapper around the react-router Routes.

Thanks a bunch!

Issa

laheadle commented 4 years ago

Is this solution the same as, or different from carloslfu/use-router-machine?

carloslfu commented 4 years ago

Yes @laheadle, thank you for pointing it out, I am going to archive carloslfu/use-router-machine.