FormidableLabs / freactal

Clean and robust state management for React and React-like libs.
MIT License
1.65k stars 46 forks source link

Subscribing to state changes #36

Open dannief opened 7 years ago

dannief commented 7 years ago

I am wondering if there is a plan to add a subscription or side effects api. The use case would be to execute functions on state change outside of component rendering. Example: syncing with local storage.

xcoderzach commented 7 years ago

I think you can you do that with middleware?

dannief commented 7 years ago

@xcoderzach From the documentation, with middleware, you can change state and decorate effects to add new functionality. I couldn't see how this might be used to subscribe to change states though.

However, I am now seeing from the source code and by inspecting the freactal context passed in the middleware function, that the context exposes a subscribe function. This function appears to take another function as argument that is passed the key of the state that has changed. This is exactly what I am looking for and it seems to work how I would expect.

This function isn't documented though. Is this what you were referring to?

Edit: Well not exactly what I was looking for. Based on how the middleware function currently works, it is called on every state change, which means I add a new subscription function for every state change. I can work around this by creating a variable that tracks whether I have already added the subscription, to ensure it is only added the first time a state changes , but this does seem a bit hacky. So I am unsure I am using the API as it was intended. See this related issue: #33

zebulonj commented 7 years ago

@dannief Middleware fires every time state changes, so it should be possible with middleware (as @xcoderzack suggests). I think you would have to be responsible for detecting what state had changed within the middleware.

The internals of freactal appear to contain machinery that does track what state has changed. Maybe those could be exposed in a way more suitable to your use case.

drcmda commented 7 years ago

That would also be a criteria for us without which we couldn't use it. We have generic classes reacting on state changes, executing actions, etc. Middleware isn't good enough and makes it very restrictive. It needs a simple getState/subscribe functionality. This will allow it to be used without components or in all frameworks and circumstances.

divmain commented 7 years ago

@drcmda, can you explain more fully what you need? Freactal is designed to be used through its higher-order components, and it is unlikely that we'll expose lower-level APIs for subscribing to state changes. However, if I could better understand the use cases you have in mind, we might be able to find a good solution, and I may change my mind if your use case is compelling :)

divmain commented 7 years ago

@dannief, the middleware is probably the piece I thought through the least. Definitely open to improvement here.

Right now, middleware is a function of signature fn(context) -> context. However, that could be changed to fn(providerInstance) -> fn(context) -> context. Each middleware function that is provided to the provideState HoC would need to return a function that transforms context when updates occur. Thoughts?

nstadigs commented 7 years ago

You could create a react component that handles this.

import React, { Component, Children } from 'react';

import { injectState } from 'freactal';

const SyncronizeTodos = injectState(
  class TodoSyncer extends Component {
    constructor(props) {
      super(props);
    }

    componentDidMount() {
      // Subscribe to things
    }

    componentWillReceiveProps({ state }) {
      // Do something whenever local state changes
    }

    componentWillUnmount() {
      // Unsubscribe from things
    }

    render() {
      return this.props.children ? Children.only(this.props.children) : null;
    }
  }
);

export default SyncronizeTodos;

While this component is mounted in the tree it will syncronize stuff for you.

Component all the things! [INSERT MEME HERE]

zbuttram commented 7 years ago

I hadn't thought about it when I first looked at freactal, but it seems like the missing piece here is almost something like MobX's autorun: https://mobx.js.org/refguide/autorun.html I don't use it very often when working with MobX but it does come in handy and I think @dannief's localStorage example is spot-on.

dannief commented 7 years ago

@divmain Could you elaborate a little on your thoughts for changing the signature of the middleware function?

If the middleware function returns a function that is executed on state changes, does this mean that you would (1) execute the top level middleware function once, and (2), store the resultant function to be executed on state changes?.

Also, would the providerInstance be the object that was originally passed to provideState?

Edit: I think the trouble I am having is, with the exception of needing to modify the state in a global way after every state change, I can't think of reason you would want to modify the effects or subscribe in a similar way. What I mean is, I am thinking the middleware function need only be executed once, to add subscriptions and wrap effects with other functionality. The only reason to return another function from the middleware function would be to register state changes. Or am I missing something?

@nbostrom That works. I actually like that pattern, though It may not be ideal for all use cases.

divmain commented 7 years ago

@dannief, middleware can also transform the state object itself, injecting or modifying values as desired. Because of this, something needs to be running on every state change.

Your summary of my thoughts is accurate. The outer function would be invoked once on provider construction. The inner/returned function would be invoked on every state change, much like the middleware behaves right now.

This approach could also be used to solve #43.

bingomanatee commented 6 years ago

Since you can store anything in a state hash, couldn't you store a mobX observable in it and begin watching it in initialize?

bingomanatee commented 6 years ago

that being said - a parameter in the middleware that told us what changed since the last cycle could allow us to write much more effective middleware.