reactn icon indicating copy to clipboard operation
reactn copied to clipboard

Subscribe to deep-nested global state properties.

Open theKashey opened this issue 6 years ago • 10 comments

Object key usage tracking is working only one level down, thus would be a source of performance issues for a wide set of cases, normally manageably by Redux.

Take a look how it was made in react-easy-state. Unfortunately (to you), that a-year-old-library is much more usable than this brand-new

theKashey avatar Nov 01 '18 21:11 theKashey

Hi @theKashey,

The Object key listener is deliberately shallow. Can you elaborate on the performance issues that you believe this to cause? I am aware that it will cause re-renders on components that access global.x.y when global.x.z updates. I am open to alternative implementations, but most seem to come with greater drawbacks than shallow listeners. Are there other concerns?

react-easy-state uses Proxies, which cannot be polyfilled and are not supported by 13% of users. They are a great solution and are absolutely the future of global state management. When browsers inevitably adopt better support for them, I am excited at the opportunity to use them in my projects, even this one. I intend to instate Proxies when support surpasses 95%, but I may do it before then with a fallback to the current system.

I do want to be clear that ReactN deliberately does not aim to be as full-featured as Redux. There are trade offs of meaningful features in exchange for a shallow learning curve, less boilerplate, and more maintainable code. It is up to each project to determine what is more important. There is no one global state management solution that applies to every project. 👍

I would not personally consider react-easy-state more usable, but I am glad to receive community feedback on how an intuitive implementation would look! If you have suggestions for what aspects of their implementation you think would improve ReactN's user experience, please share them.

Thanks for taking the time to look at the repository. I look forward for more feedback.

quisido avatar Nov 01 '18 22:11 quisido

I have added the behavior to the documentation here. Let me know if this closes your issue.

quisido avatar Nov 01 '18 22:11 quisido

You may just keep tracking nested keys. At lest for objects, as long for arrays without proxy that could be not so easy. Tracking is not a big problem by itself, it's much harder to manage result. Like

<div>{global.x}</div> - I should update on `x` change
<div>{global.x.y}</div> - I should update on `x.y` change, but could ignore `x` chage
<div>{global.x.y} + {global.x.z}</div> - I should update on `x.y` and `x.y` change
....
<div>{someFunction(global.x)}</div> - oh. I don't know.

To be honest - I dont know how to manage key tracking on the "render" level. Anyway - some thought could be found here - https://github.com/respond-framework/remixx/blob/master/docs/initial-plan.md

theKashey avatar Nov 01 '18 23:11 theKashey

It is not possible to know whether I accessed global.x as the entire object or global.x.y as a property. For JS to get to property y on object global.x, it must first access global.x.

const a = this.global;
const b = a.x;
const c = b.y;
return c; // I only want to subscribe to this.global.x.y changes.

Boilerplate can solve this issue, but I am strictly opposed. At that point, one should just be using Redux.

It is possible to listen to only the deepest accessed properties. If I accessed global.x.y, I can ignore subscriptions to global.x.

This would work in all cases except where both the object and a property on that object are used. I cannot think of a use case for this, outside of JSON.stringify(global.x) being used in the same component as global.x.y. I don't want to not account for that, but it's such an outlier case. I'll sleep on it and leave it open to more discussion, as well as more alternative algorithms.

quisido avatar Nov 02 '18 03:11 quisido

Optional boilerplate may be acceptable for the sole purpose of performance benefits, instead of necessity.

Something like this.globalIgnore('x') in the componentDidMount for components that access global.x.y.

It doesn't add boilerplate to the typical user case, but I'm not sure how intuitive it is.

I'm also not completely sold on adding listeners to nested objects, What if that object is an instance, such as new Promise? Returning a modified clone sounds like it can cause bugs. I do like the idea in concept, though.

I've also just opened this withGlobal HOC issue for discussion on it as a viable choice for optional boilerplate.

quisido avatar Nov 02 '18 15:11 quisido

Hey @theKashey ,

I don't mean to spam you with these discussions. I've just released deep-proxy-polyfill in an attempt to get close as possible to Proxy behavior. I may change the syntax a bit, but that would be a rough draft of how deep-nested subscriptions would work.

It's passing tests that it is subscribing correctly. Some performance needs to be taken into consideration (having getters on every parent object firing), but ultimately I think it's a good starting point.

With a community go-ahead, I'll implement it in ReactN.

quisido avatar Nov 06 '18 14:11 quisido

Looking good! If you will also use Proxy when possible - it will be just perfect!

theKashey avatar Nov 07 '18 00:11 theKashey

This would be so useful if implemented int ReactN!

nickfla1 avatar May 10 '19 07:05 nickfla1

How about using useUpdateState hook which does everything like useGlobal but it returns updateState instead of setState with API like.

[state, updateState] = useUpdateState("globalState")

updateState({action: "action-to-perfom", field: "field-to-update", value: "value-to-update"})

supported operations being assign, push, pop, remove, filter and others if there will be any, and for nested fields/keys the field parameter is specified as x.y.z.

yezyilomo avatar Jun 22 '19 18:06 yezyilomo

useUpdateState in action.

setGlobal({
    user: {isLoggedIn: true, info: {name: "peter", emails: ["[email protected]"]}, }
})

function User(props){
    [user, updateUser] = useUpdateState("user");
    
    let editName = () => {
        updateUser({action: "assign", field: "info.name", value: "jason"})
        // Or simply updateUser({field: "info.name", value: "jason"})
    }
    
    let addEmail = () => {
        updateUser({action: "push", field: "info.email", value: "[email protected]"})
    }
    
    let removLastEmail = () => {
        updateUser({action: "pop", field: "info.email"})
    }
    
    let removeEmail = () => {
        updateUser({action: "remove", field: "info.email", value: "[email protected]"})
    }
    
    let filterEmails = () => {
        updateUser({action: "filter", field: "info.email", value: (email) => email != "[email protected]"})
    }
    
    let logOutUser = () => {
        updateUser({action: "assign", field: "isLoggedIn", value: false})
        // Or simply updateUser({field: "isLoggedIn", value: false})
    }
}

yezyilomo avatar Jun 22 '19 19:06 yezyilomo