statelyai / xstate

Actor-based state management & orchestration for complex app logic.
https://stately.ai/docs
MIT License
26.89k stars 1.23k forks source link

XState + Mobx/Redux #37

Closed AKuederle closed 5 years ago

AKuederle commented 6 years ago

Hi, I was wondering how this library is intended to work together with other state management tools. I am not really experience with any of these tools, but from my understanding and your comments in #15, I assume that you could use xState to handle the immutable part of your state, while using another form of state management for the rest of your state.

Based on that I tried to modified the example provided in the Readme and refactored it to use a Mobx Store instead of setState. I would be really interested in your opinion about the general implementation approach and if you like I can add the example to the Readme with some comments.

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { observable, action, useStrict, runInAction } from 'mobx'
import { Machine } from 'xstate'

useStrict(true)

const ROOT_URL = `https://api.github.com/users`
const myMachine = Machine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        CLICK: 'loading'
      }
    },
    loading: {
      on: {
        RESOLVE: 'data',
        REJECT: 'error'
      }
    },
    data: {
      on: {
        CLICK: 'loading'
      }
    },
    error: {
      on: {
        CLICK: 'loading'
      }
    }
  }
})

class AppState {
  @observable data = {}
  @observable dataState = 'idle'
  @observable input = ''

  commands = {
    loading: this.searchRepositories
  }

  @action
  transition (action) {
    console.log(this.dataState, action)
    const newState = myMachine.transition(this.dataState, action).value
    const command = this.commands[newState]

    this.dataState = newState

    if (command) {
      command.call(this)
    }
  }

  @action
  async searchRepositories (){
    try {
      const data = await fetch(`${ROOT_URL}/${this.input}`).then(response => response.json())
      runInAction(() => {
        this.data = data
        console.log(this.data)
        this.transition('RESOLVE')  
      })
    } catch (error) {
      runInAction(() => {
        this.transition('REJECT')
      })
    }
  }

  @action changeText(text) {
    this.input = text
  }
}

const store = new AppState()

@observer
class App extends Component {
  render() {
    const { data, dataState } = store
    const buttonText = {
      idle: 'Fetch Github',
      loading: 'Loading...',
      error: 'Github fail. Retry?',
      data: 'Fetch Again?'
    }[dataState]
    return (
      <div>
        <input
          type="text"
          value={store.input}
          onChange={e => store.changeText(e.target.value)}
        />
        <button
          onClick={() => store.transition('CLICK')}
          disabled={dataState === 'loading'}
        >
          {buttonText}
        </button>
        {dataState === 'error'?
          <h1>Error!!!</h1> :
          data && <div>{JSON.stringify(data, null, 2)}</div>}
      </div>
    )
  }
}

export default App
carlbarrdahl commented 6 years ago

I created a small redux middleware to use with xstate: https://github.com/carlbarrdahl/redux-xstate

const stateChart = {
  key: "light",
  initial: "off",
  states: {
    on: {
      on: { SWITCH: "off" },
      onEntry: ["notify"]
    },
    off: {
      on: { SWITCH: "on" }
    }
  }
}
const actionMap = {
  notify: (dispatch, state, { payload: { timeout } }) => {
    console.log("light was just switched on")
    if (timeout) {
      console.log("switching off in %s ms", timeout)
      setTimeout(() => dispatch({ type: "SWITCH" }), timeout)
    }
  }
}

const machine = Machine(stateChart)

const store = createStore(
  combineReducers({
    machine: createReducer(machine.initialState)
  }),
  applyMiddleware(createMiddleware(machine, actionMap))
)

store.dispatch({
  type: "SWITCH",
  payload: { timeout: 1000 }
})
davidkpiano commented 5 years ago

Closing this, as XState itself can be used as a state management tool, and with the interpreter emitting updated state, it can be consumed in any state management tool by listening for state changes.

RuiOrey commented 4 years ago

@davidkpiano I have a question about XState and Redux differences. I will write it here but I can move to a new issue or other place if you think does not belong here. Basically I left the question on stack overflow also but I will quote myself:

I've been using Redux for most of my React applications. I think I will start using XState as I don't have to use effects as a plugin all the time. And I think is a more complete pattern. One thing that I want to understand is it's connection with React (hooks and classes) and it's interaction with reactive programming in general: Can I (and should I) use XState context as Redux data store in the same way, having a single source of truth on a shared by React Components way? Will my components be able to "connect" and "mapToProps" the XState context and rerender only when those values changes and not every time the state machine state changes? From what I understand Redux lacking side effects is so it can adhere to a pure functional paradigm. But that breaks with side effects usage, that is a lot of times needed in web apps or games for example.

can't I just make an HOC to do the same as react-redux connect() and map XState's send and parts of the context to React's props?Or there is better way to do this on XState? XState already seems to have a good integration with Redux Dev tools by itself.

Basically I know I can use onTransition to map context changes to react's state. But let's say I want to have clean components (using classes for now) and I want part of the context only, by making an HOC I can fully replace redux, right? Should I use sub machines for each component state? Actors? I've been testing and I like it a lot but I want to understand a good, advisable and clean way to integrate with React in a scalable app. And replace redux and side effects libs. Thanks

davidkpiano commented 4 years ago

@RuiOrey This would be a good conversation for spectrum.chat/statecharts, but in general yes use a machine (like local state) for each component.

RuiOrey commented 4 years ago

Thanks, will question there