foxdonut / meiosis

meiosis
https://meiosis.js.org
MIT License
415 stars 18 forks source link

Does it necessary to pass models from root down to every component to make meiosis pattern works ? #22

Closed meepeek closed 6 years ago

meepeek commented 6 years ago

@foxdonut Please help

Refer to the example I have created: https://github.com/meepeek/meiosis-react-example , the component can be dependent on their own by the work of react setState and not meiosis itself. In temperature example, the update and models passed thought from root through every components which makes the code so complex.

It would be hard to scale if we have to pass everything down to make it works. I still cannot find the solution.

Can we have 2 updates in a single app without setState ?

foxdonut commented 6 years ago

@meepeek it really depends on what you are using and how you want to structure your code. Using setState is definitely not necessary when using Meiosis with React. It is simpler to just have views as functions of the model.

That being said, you can still use Meiosis with React components that rely on setState, be it 3rd party components or your own components if you decide that you want some state to only be local to that component, instead of in the top-level model.

Passing down the model to components does not make things hard to scale. It's actually easier and more consistent, because you always just call view(model). Nesting makes it possible to work with their own subset of the model while still having a single top-level model. This is easier to manage than having to pass multiple individual attributes, or spread properties, as is typical in many React examples.

You will find many examples:

Hope that helps!

meepeek commented 6 years ago

@foxdonut I try to combine scan but there was no function on flyd that match my need. Could you guide me ? Here's my code (apart from App.js, everything is the same as https://github.com/meepeek/meiosis-react-example):

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import testMComponent from './testMComponent'

import meiosisTracer from "meiosis-tracer";
import flyd from 'flyd'
import scanMerge from 'flyd-scanmerge'

const componentA = new testMComponent()
const componentB = new testMComponent({click:55})

const mixUpdate = flyd.combine((a, b) => { return {a, b} }, [componentA.update, componentB.update])
// const mixUpdate = flyd.merge(componentA.update, componentB.update)

const mixListener = flyd.scan((o, n) => { window.temp = {o,n}; return o }, {
  a: componentA.listener(),
  b: componentB.listener()
}, mixUpdate)
mixListener.map(v => console.log(v))
window.temp = mixListener
// window.mixUpdate = mixUpdate
window.mixListener  = mixListener
meiosisTracer({ selector: "#tracer", streams: [ mixListener, componentA.listener ] });

class App extends Component {
  constructor(props) {
    super(props);
    const self = this;

    self.state = {
      a: componentA.listener(),
      b: componentB.listener()
    };

    self.actions = props.actions
    self.models = props.models

  }

  componentDidMount() {
    const self = this;

    mixListener.map( model => self.setState({model}) )
    // componentA.listener.map(model => {
    //   self.setState({ a: model });
    // });
    // componentB.listener.map(model => {
    //   self.setState({ b: model });
    // });
  }

  render() {
    return (
      <div className="App">
      {componentA.view(this.state.a)}
      {componentB.view(this.state.b)}
      </div>
    );
  }
}

export default App;

What I'm trying to do is encapsulate multiple scans into a single scans. (in this case I named the scan as listener)

foxdonut commented 6 years ago

Hi @meepeek

Instead of combining multiple scans into a single scan, you should use Nesting.

Here's how it works with your example.

  1. index.js
import ReactDOM from 'react-dom';
import './index.css';
import createApp from './App';
import registerServiceWorker from './registerServiceWorker';

import flyd from 'flyd'
import meiosisTracer from "meiosis-tracer";

const update = flyd.stream();
const app = createApp(update)
const models = flyd.scan((model, updateFunc) => updateFunc(model), app.model(), update);

models.map(model => ReactDOM.render(app.view(model), document.getElementById('root')))
registerServiceWorker();

meiosisTracer({ selector: "#tracer", streams: [ models ] });

const actions = {
  test: {
    click: () => {
      update(model => {model.a.click++; return model})
    },
    typing: (value) => {
      update(model => {model.a.click = value; return model})
    }
  }
}

window.models = models
window.actions = actions
window.update = update
  1. App.js
import React from 'react'
import './App.css'
import { createTestMComponent } from './testMComponent'
import { nestComponent } from './nest'

const createApp = update => {
  const componentA = nestComponent(createTestMComponent, update, ["a"])
  const componentB = nestComponent(createTestMComponent, update, ["b"])

  return {
    model: () => Object.assign({},
      componentA.model(),
      componentB.model({click:55})
    ),

    view: model => (
      <div className="App">
        {componentA.view(model)}
        {componentB.view(model)}
      </div>
    )
  }
}

export default createApp
  1. testMComponent.jsx
import React from 'react';

export const createTestMComponent = update => {
  const actions = {
    typing: (v) => {
      update(model => {model.click = v; return model})
    },
    click: () => update(model => {model.click++; return model})
  }

  return {
    model: init => init || ({
      click: 0
    }),

    view: model => (
      <div>
        <input type="text" value={model.click} onChange={evt => actions.typing(evt.target.value)} />
        <button onClick={ evt => {evt.preventDefault(); actions.click()} } />
      </div>
    )
  }
}
  1. nest.js
import _ from "lodash"

const nestUpdate = (update, path) => func =>
  update(model => _.update(model, path, func))

export const nestComponent = (create, update, path) => {
  const component = create(nestUpdate(update, path));
  const result = Object.assign({}, component);

  if (component.model) {
    result.model = init => _.set({}, path, component.model(init));
  }
  if (component.view) {
    result.view = model => component.view(_.get(model, path));
  }
  return result;
};

I'm using Lodash as a utility library, but of course you can use something else, if you prefer.

Hope that helps.