New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ContextMixin #107

Closed
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
3 participants
@christianalfoni
Contributor

christianalfoni commented Mar 1, 2015

Hi there,

Okay, for background on this suggestion, read the following article: True isomorphic apps with React and Baobab

I thought it could be used like this:

var ContextMixin = require('baobab/ContextMixin');

That way it is completely separate from Baobab itself, which it has to be, but still a part of the package to give more functionality. Much like react/addons.

I mentioned this pull request in the article, so maybe discuss it a bit more first. But this is truly truly awesome stuff :-)

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Mar 2, 2015

Owner

Ok, let met read this and document myself about react contexts. I will probably add remarks on the fly while I am perusing the article.

Owner

Yomguithereal commented Mar 2, 2015

Ok, let met read this and document myself about react contexts. I will probably add remarks on the fly while I am perusing the article.

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Mar 2, 2015

Owner

When loading the application component on the server it will load all the dependencies of each component also. If this dependency is a state tree the component registers listeners to, you will get into problems. You do not want to do this on the server, as you would get a huge memory leak and you will have multiple users wanting to load your application with different states, a single tree will not cut it.

There is a tree.release method that is able to clean listeners and anything attached to the tree in order to avoid memory leaks in those kind of cases.

Owner

Yomguithereal commented Mar 2, 2015

When loading the application component on the server it will load all the dependencies of each component also. If this dependency is a state tree the component registers listeners to, you will get into problems. You do not want to do this on the server, as you would get a huge memory leak and you will have multiple users wanting to load your application with different states, a single tree will not cut it.

There is a tree.release method that is able to clean listeners and anything attached to the tree in order to avoid memory leaks in those kind of cases.

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Mar 2, 2015

Owner

So, here is what I don't understand:

Why isn't it possible to just hydrate a baobab tree server-side with the necessary state to render your application? Then you can send the state along with the rendered app as you suggest at the end of your article.

Owner

Yomguithereal commented Mar 2, 2015

So, here is what I don't understand:

Why isn't it possible to just hydrate a baobab tree server-side with the necessary state to render your application? Then you can send the state along with the rendered app as you suggest at the end of your article.

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 2, 2015

Contributor

You know what, I have been sitting here for like 5 minutes trying to come up with an argument :-)

It would indeed be nice with:

server

var store = require('./app/store.js'); // Baobab tree instantiated
var App = React.factory(require('./app/App.jsx'));

app.get('/', function (req, res) {
  store.hydrate(createAppState());
  var html = React.renderToString(App());
  var state = store.dehydrate(); // Removes listeners and returns the state
  // Bootstrap state and app, and send response
});

What I do think is an argument though is that if you start to go down this path you will probably load your whole application on the server, which could potentially become an issue. What I mean is that if you start out with your state tree as an "outer dependency", your actions for changing state in the state tree will also become an "outer dependency". Those actions depends on other modules again that are specific to the client.

What is very nice about the strategy explained in the article is that you control the context of the applications execution. On the server you will only load the components and the mixin, not your client side state tree definition, actions and other dependencies of those. You strictly load the components. That I see as a huge advantage. Its like making your components as pure as possible and as unrelated to any context as possible. That is decided when the application is run, not by any single component. Does that make sense? It is a bit abstract, and it is more of an intuition than hard facts :-)

That said, I think it would be valuable to have a hydrate and dehydrate method, something like the example above? That would indeed let you easily reuse your client side components on the server.

I have to give this some more thought. What do you think? :-)

Contributor

christianalfoni commented Mar 2, 2015

You know what, I have been sitting here for like 5 minutes trying to come up with an argument :-)

It would indeed be nice with:

server

var store = require('./app/store.js'); // Baobab tree instantiated
var App = React.factory(require('./app/App.jsx'));

app.get('/', function (req, res) {
  store.hydrate(createAppState());
  var html = React.renderToString(App());
  var state = store.dehydrate(); // Removes listeners and returns the state
  // Bootstrap state and app, and send response
});

What I do think is an argument though is that if you start to go down this path you will probably load your whole application on the server, which could potentially become an issue. What I mean is that if you start out with your state tree as an "outer dependency", your actions for changing state in the state tree will also become an "outer dependency". Those actions depends on other modules again that are specific to the client.

What is very nice about the strategy explained in the article is that you control the context of the applications execution. On the server you will only load the components and the mixin, not your client side state tree definition, actions and other dependencies of those. You strictly load the components. That I see as a huge advantage. Its like making your components as pure as possible and as unrelated to any context as possible. That is decided when the application is run, not by any single component. Does that make sense? It is a bit abstract, and it is more of an intuition than hard facts :-)

That said, I think it would be valuable to have a hydrate and dehydrate method, something like the example above? That would indeed let you easily reuse your client side components on the server.

I have to give this some more thought. What do you think? :-)

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Mar 3, 2015

Owner

Ok, so here is current opinion about Baobab and how I try to use it:

  1. I think user interfaces should be considered as pure functions as a whole (there might be side-effects triggered by events, for instance, but this is another problem). For me, Baobab, by storing the app's centralized state, is in fact a way to provide needed arguments to this pure function. Therefore, given a precise state, your UI will always render identically. This comes with very useful collateral advantages like a very simple way to save the app's state in the localStorage or easy undo/redo features etc.

  2. External dependency to the store is indeed a problem and usually, when I need for a component to remain generalizable, I do not plug it to the store and only use generic props & state combination that will be given by its parent, himself plugged to the store.

  3. I do not use Flux per se with Baobab and only use a thin event-emitting layer along with it to ensure, that components will not edit the store by themselves but will refer to a "central authority" through an event. (Your way to pass actions with the context is another elegant way to deal with this however). So, in most of cases (by this I mean cases I experimented), actions are an outside dependency which is only related to the client-side of the equation and that will never run server-side.

  4. Isomorphic should not be an issue since your interface is a pure function taking as sole argument a given state (stored in a Baobab tree, here). It should render the exact same on the server and on the client.

What I have trouble understanding here is rather the benefit we can get out of React's context in this particular case. I like the idea of making the app's components as pure as needed but isn't the context itself another name for our "outside" dependencies" since without this context, you won't be able to run the app? Or should we consider the context as an abstraction layer between the combo store/controller and the components?

Owner

Yomguithereal commented Mar 3, 2015

Ok, so here is current opinion about Baobab and how I try to use it:

  1. I think user interfaces should be considered as pure functions as a whole (there might be side-effects triggered by events, for instance, but this is another problem). For me, Baobab, by storing the app's centralized state, is in fact a way to provide needed arguments to this pure function. Therefore, given a precise state, your UI will always render identically. This comes with very useful collateral advantages like a very simple way to save the app's state in the localStorage or easy undo/redo features etc.

  2. External dependency to the store is indeed a problem and usually, when I need for a component to remain generalizable, I do not plug it to the store and only use generic props & state combination that will be given by its parent, himself plugged to the store.

  3. I do not use Flux per se with Baobab and only use a thin event-emitting layer along with it to ensure, that components will not edit the store by themselves but will refer to a "central authority" through an event. (Your way to pass actions with the context is another elegant way to deal with this however). So, in most of cases (by this I mean cases I experimented), actions are an outside dependency which is only related to the client-side of the equation and that will never run server-side.

  4. Isomorphic should not be an issue since your interface is a pure function taking as sole argument a given state (stored in a Baobab tree, here). It should render the exact same on the server and on the client.

What I have trouble understanding here is rather the benefit we can get out of React's context in this particular case. I like the idea of making the app's components as pure as needed but isn't the context itself another name for our "outside" dependencies" since without this context, you won't be able to run the app? Or should we consider the context as an abstraction layer between the combo store/controller and the components?

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 4, 2015

Contributor

Hi again :-)

  1. Word! ;-)

  2. Yeah, I saw the strategy presented on react-conf. You use a wrapper that grabs store state and give children access through props. It makes sense, though I feel it is only "half way there". Ideally I do not want any of my components to depend on the store

  3. That is actually a very good idea, and I think it is very flux like actually. You emit an event through a hub (dispatcher) and something else might be registered to handle that event, much like a store registers to a dispatcher and filters out the actions (events) it wants to act upon. But I like your strategy better, it is just simpler, more basic, and as decoupled as you can get it

  4. Jup

That is a very good question! I have been thinking about this a bit and what we basically have here is two strategies:

  1. External store and event hub
    You have a store module and an event module. Your components rely on both of them as external dependencies to get and change state. In an isomorphic implementation this works great because the components will only have two specific dependencies. store and event hub. To handle this on the server you require the same store as all your components does and hydrate it. Then you render the app before dehydrating (releasing) it. I think this is an excellent strategy as it does not require any special mixin.
  2. Run with context
    You have a store module and actions module, but your components does not rely on any of them. The only thing your components rely on is a context where a store object and actions object is available. The only argument I can give here is that you do get as pure components as possible, but I am having a hard time finding a good argument for that to be a practical advantage :-)

I think it would be better to ignore this pull request and rather write about how one would do isomorphic apps with Baobab and an event hub. I got the day off tomorrow, so I thought I would find a café and rewrite the documentation into the WIKI here.

Contributor

christianalfoni commented Mar 4, 2015

Hi again :-)

  1. Word! ;-)

  2. Yeah, I saw the strategy presented on react-conf. You use a wrapper that grabs store state and give children access through props. It makes sense, though I feel it is only "half way there". Ideally I do not want any of my components to depend on the store

  3. That is actually a very good idea, and I think it is very flux like actually. You emit an event through a hub (dispatcher) and something else might be registered to handle that event, much like a store registers to a dispatcher and filters out the actions (events) it wants to act upon. But I like your strategy better, it is just simpler, more basic, and as decoupled as you can get it

  4. Jup

That is a very good question! I have been thinking about this a bit and what we basically have here is two strategies:

  1. External store and event hub
    You have a store module and an event module. Your components rely on both of them as external dependencies to get and change state. In an isomorphic implementation this works great because the components will only have two specific dependencies. store and event hub. To handle this on the server you require the same store as all your components does and hydrate it. Then you render the app before dehydrating (releasing) it. I think this is an excellent strategy as it does not require any special mixin.
  2. Run with context
    You have a store module and actions module, but your components does not rely on any of them. The only thing your components rely on is a context where a store object and actions object is available. The only argument I can give here is that you do get as pure components as possible, but I am having a hard time finding a good argument for that to be a practical advantage :-)

I think it would be better to ignore this pull request and rather write about how one would do isomorphic apps with Baobab and an event hub. I got the day off tomorrow, so I thought I would find a café and rewrite the documentation into the WIKI here.

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 5, 2015

Contributor

Could of course link to the article for a different take on the same strategy :-)

Contributor

christianalfoni commented Mar 5, 2015

Could of course link to the article for a different take on the same strategy :-)

@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 13, 2015

A quick question guys, probably somehow related.
When you say actions are not needed on the server I'm wondering how you go about constructing the state there? Because thinking that all the async business, like talking to an API, would sit inside the actions I would like to re-use them also server side. I could call them from inside static methods on my react-router handler components.
Do you see another option here?

nickdima commented Mar 13, 2015

A quick question guys, probably somehow related.
When you say actions are not needed on the server I'm wondering how you go about constructing the state there? Because thinking that all the async business, like talking to an API, would sit inside the actions I would like to re-use them also server side. I could call them from inside static methods on my react-router handler components.
Do you see another option here?

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 13, 2015

Contributor

hi @nickdima ,

Lets look at a specific example. You have a list of todos. Normally you would start out with an empty array on the client. To fill up that array you would trigger an action that does an ajax call to the server to grab the todos. That action could be triggered when you render the app and it might be used to update the todos with an interval. That logic is specific to the client, an ajax call to the server on the server does not really make sense and you would not want to trigger an interval to fetch todos on the server. So that is what I mean by "not trigger actions on the server".

When rendering the app on the server you would first grab the state you need directly from the database, a cache etc. and put that into the app, and optionally bootstrap it so that you do not have to do the initial fetch on the client. Something like this:

var index = fs.readFileSync(path.resolve(__dirname, 'index.html')).toString();

app.get('/', function (req, res) {

  // First we build up the contents of the store. This could be fetching database stuff
  // or whatever
  fetchTodos()
    .then(function (todos) {

      var store = {todos: todos};

      // We render the application to an HTML string, passing in the store
      var app = React.renderToString(AppWrapper({store: store}));

      // We replace our placeholders with both the app html itself and the state of the store. 
      // You might prefer loading the store state with ajax after initial page load in case 
      // it is quite large. It is a balance you have to decide
      var html = index.replace('{{APP}}', app).replace('{{STORE}}', JSON.stringify(store));

      res.type('html');
      res.send(html);
    });

});
app.listen(3000);

I hope I understood your question correctly and this made sense :-)

Contributor

christianalfoni commented Mar 13, 2015

hi @nickdima ,

Lets look at a specific example. You have a list of todos. Normally you would start out with an empty array on the client. To fill up that array you would trigger an action that does an ajax call to the server to grab the todos. That action could be triggered when you render the app and it might be used to update the todos with an interval. That logic is specific to the client, an ajax call to the server on the server does not really make sense and you would not want to trigger an interval to fetch todos on the server. So that is what I mean by "not trigger actions on the server".

When rendering the app on the server you would first grab the state you need directly from the database, a cache etc. and put that into the app, and optionally bootstrap it so that you do not have to do the initial fetch on the client. Something like this:

var index = fs.readFileSync(path.resolve(__dirname, 'index.html')).toString();

app.get('/', function (req, res) {

  // First we build up the contents of the store. This could be fetching database stuff
  // or whatever
  fetchTodos()
    .then(function (todos) {

      var store = {todos: todos};

      // We render the application to an HTML string, passing in the store
      var app = React.renderToString(AppWrapper({store: store}));

      // We replace our placeholders with both the app html itself and the state of the store. 
      // You might prefer loading the store state with ajax after initial page load in case 
      // it is quite large. It is a balance you have to decide
      var html = index.replace('{{APP}}', app).replace('{{STORE}}', JSON.stringify(store));

      res.type('html');
      res.send(html);
    });

});
app.listen(3000);

I hope I understood your question correctly and this made sense :-)

@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 13, 2015

Yeah, your example makes sense for some types of apps. But when you need to call the same API both on the client and the server to get the data a good case of reusing this logic is valid.

nickdima commented Mar 13, 2015

Yeah, your example makes sense for some types of apps. But when you need to call the same API both on the client and the server to get the data a good case of reusing this logic is valid.

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 13, 2015

Contributor

Hi again @nickdima , ah, yeah, that is a good point indeed, but I do not think you can handle this exactly the same on the client and server. The reason is that when you render your app on the server that is done synchronously. If it depends on some async data that has to be resolved before rendering the app. If there is an action that is triggered while rendering the app you would get into trouble.

I think the best solution is to share that action as a module. Allow for both the client to use it as an action, but also allow the server to require the same module, running it before you render the client on the server. Something like:

var index = fs.readFileSync(path.resolve(__dirname, 'index.html')).toString();
var publicAPIAction = require('./sharedModules/publicAPIAction.js');

app.get('/', function (req, res) {

  // First we build up the contents of the store. This could be fetching database stuff
  // or whatever
 Promise.all([
    publicAPIAction(),
    fetchTodos()
  ])
    .then(function (results) {

      var store = {publicAPIData: results[0], todos: results[1]};

      // We render the application to an HTML string, passing in the store
      var app = React.renderToString(AppWrapper({store: store}));

      // We replace our placeholders with both the app html itself and the state of the store. 
      // You might prefer loading the store state with ajax after initial page load in case 
      // it is quite large. It is a balance you have to decide
      var html = index.replace('{{APP}}', app).replace('{{STORE}}', JSON.stringify(store));

      res.type('html');
      res.send(html);
    });

});
app.listen(3000);
Contributor

christianalfoni commented Mar 13, 2015

Hi again @nickdima , ah, yeah, that is a good point indeed, but I do not think you can handle this exactly the same on the client and server. The reason is that when you render your app on the server that is done synchronously. If it depends on some async data that has to be resolved before rendering the app. If there is an action that is triggered while rendering the app you would get into trouble.

I think the best solution is to share that action as a module. Allow for both the client to use it as an action, but also allow the server to require the same module, running it before you render the client on the server. Something like:

var index = fs.readFileSync(path.resolve(__dirname, 'index.html')).toString();
var publicAPIAction = require('./sharedModules/publicAPIAction.js');

app.get('/', function (req, res) {

  // First we build up the contents of the store. This could be fetching database stuff
  // or whatever
 Promise.all([
    publicAPIAction(),
    fetchTodos()
  ])
    .then(function (results) {

      var store = {publicAPIData: results[0], todos: results[1]};

      // We render the application to an HTML string, passing in the store
      var app = React.renderToString(AppWrapper({store: store}));

      // We replace our placeholders with both the app html itself and the state of the store. 
      // You might prefer loading the store state with ajax after initial page load in case 
      // it is quite large. It is a balance you have to decide
      var html = index.replace('{{APP}}', app).replace('{{STORE}}', JSON.stringify(store));

      res.type('html');
      res.send(html);
    });

});
app.listen(3000);
@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 13, 2015

Not sure if you're familiar with react-router but here's how data fetching is usually implemented server side on projects that use it: https://github.com/rackt/react-router/blob/master/examples/async-data/app.js
Basically the route handler components have this fetchData static method that gets called for every route matched by the routes. (you could have fetch functions on multiple level of nested routes)

nickdima commented Mar 13, 2015

Not sure if you're familiar with react-router but here's how data fetching is usually implemented server side on projects that use it: https://github.com/rackt/react-router/blob/master/examples/async-data/app.js
Basically the route handler components have this fetchData static method that gets called for every route matched by the routes. (you could have fetch functions on multiple level of nested routes)

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 20, 2015

Contributor

Sorry for late response, been down with the flue!

I was not aware of that functionality with react-router, nice! That is definitely a way to go. I favor using a basic router like Page.js, as I see my routing as state like any other state. But again, this depends on the project and what you feel comfortable with.

What I can not quite understand though is why you would want to call the server using ajax, on the server. Like: /tasks. This is a route on the server. Why would you want to call that with ajax both on the client and the server?

Contributor

christianalfoni commented Mar 20, 2015

Sorry for late response, been down with the flue!

I was not aware of that functionality with react-router, nice! That is definitely a way to go. I favor using a basic router like Page.js, as I see my routing as state like any other state. But again, this depends on the project and what you feel comfortable with.

What I can not quite understand though is why you would want to call the server using ajax, on the server. Like: /tasks. This is a route on the server. Why would you want to call that with ajax both on the client and the server?

@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 20, 2015

@christianalfoni in a lot of cases (also in mine) the server is just a light node.js layer that calls an API that sits somewhere else. Using a http request library that works both on node and the browser, like superagent, you can reuse the code that makes the API calls.

nickdima commented Mar 20, 2015

@christianalfoni in a lot of cases (also in mine) the server is just a light node.js layer that calls an API that sits somewhere else. Using a http request library that works both on node and the browser, like superagent, you can reuse the code that makes the API calls.

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Mar 23, 2015

Owner

Little question for you @christianalfoni, since you are skilled with contexts :). Would it be possible to propagate the context from top element to bottom without having to use a mixin on every components in the hierarchy?

Owner

Yomguithereal commented Mar 23, 2015

Little question for you @christianalfoni, since you are skilled with contexts :). Would it be possible to propagate the context from top element to bottom without having to use a mixin on every components in the hierarchy?

@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 23, 2015

@Yomguithereal the context is propagated down the tree from the component that sets it using getChildContext. You just have to specify the contextTypes on the component where you need it. More info here: https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html

nickdima commented Mar 23, 2015

@Yomguithereal the context is propagated down the tree from the component that sets it using getChildContext. You just have to specify the contextTypes on the component where you need it. More info here: https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html

@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 23, 2015

As an API I would follow the FluxComponent from Flummox: https://github.com/acdlite/flummox/blob/master/docs/api/FluxComponent.md where instead of stores we have cursors.

nickdima commented Mar 23, 2015

As an API I would follow the FluxComponent from Flummox: https://github.com/acdlite/flummox/blob/master/docs/api/FluxComponent.md where instead of stores we have cursors.

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Mar 23, 2015

Owner

Thanks @nickdima. I think I will then switch to a generic mixin to decouple the tree instance from the app itself. I think I won't include a Flummox-like component to the core library because it would mean to add React as a dependency (or a peer one anyway). I am not opposed to try such a component as a side project however.

Owner

Yomguithereal commented Mar 23, 2015

Thanks @nickdima. I think I will then switch to a generic mixin to decouple the tree instance from the app itself. I think I won't include a Flummox-like component to the core library because it would mean to add React as a dependency (or a peer one anyway). I am not opposed to try such a component as a side project however.

@nickdima

This comment has been minimized.

Show comment
Hide comment
@nickdima

nickdima Mar 23, 2015

Yeah, I think you're right, the component could make sense as a separate project. Perhaps it could share some of the logic of the mixin?

nickdima commented Mar 23, 2015

Yeah, I think you're right, the component could make sense as a separate project. Perhaps it could share some of the logic of the mixin?

@christianalfoni

This comment has been minimized.

Show comment
Hide comment
@christianalfoni

christianalfoni Mar 24, 2015

Contributor

Hi guys,

Wrote a simple approach on this issue: #134. It takes a different approach to "upgrade" the component which also supports ES6 classes. Though the example looks like "one approach" it would be possible to use the same mixin concept as now, it just "upgrades" the component a bit differently.

Contributor

christianalfoni commented Mar 24, 2015

Hi guys,

Wrote a simple approach on this issue: #134. It takes a different approach to "upgrade" the component which also supports ES6 classes. Though the example looks like "one approach" it would be possible to use the same mixin concept as now, it just "upgrades" the component a bit differently.

@Yomguithereal

This comment has been minimized.

Show comment
Hide comment
@Yomguithereal

Yomguithereal Apr 20, 2015

Owner

Moving this discussion to baobab-react.

Owner

Yomguithereal commented Apr 20, 2015

Moving this discussion to baobab-react.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment