arqex / freezer

A tree data structure that emits events on updates, even if the modification is triggered by one of the leaves, making it easier to think in a reactive way.
MIT License
1.28k stars 56 forks source link

How would you handle a "currently selected component" state/feature with Freezer and React ? #77

Closed PlanetIrata closed 7 years ago

PlanetIrata commented 8 years ago

Hi, here is my problem, suppose that I have 3 simple React <div> components containing a simple text and one React <input> component to edit the <div> texts (the <input> is NOT a child of any <div>, it's a separate component in a sidebar). When clicking on a <div>, it has to become the "active component" : I have to put its current text in the <input> and then, when the <input> content is modified, set back in real time the content to this currently selected <div> : each time a letter is typed in the <input>, I do a set(...) to update the text of the <div> in the store, then the component tree is re-rendered and the <div> displays the new text.

I've successfully manage this by using a "currSelectedDiv" property in my app state, but the problem is that this node is muted on every letter typed in the <input>, so I can't optimize the rendering with ShouldComponentUpdate, all the 3 <div> are re-rendered on each letter typed.

I can't find a solution to resolve this. If the <input> was a child of each <div> it would be easy, but how to manage a relation between 2 components that have no relationship without re-rendering all the components of the tree ?

Hope you can help, thanks.

kwoon commented 8 years ago

I think the problem is not in the Freezer. You can use debounce function if you want to wait when user stopped text typing. And only after that try to trigger reaction which saved you state.

arqex commented 8 years ago

Hi,

This is like a freezer code cata, and here it is my solution.

http://jsbin.com/vecozewuko/1/edit?js,output

It's sure that you will need to store the values of your divs separately. I have used an array for those values, and a simple index to know what's the selected div.

When input values are stored in freezer directly, the live mode of freezer is needed since the user can type more than one character in the same tick and that would make your app lost those characters.

var store = new Freezer({...}, {live: true});

Keep in mind that it is a simple example, you might want to use reactions in order to keep your code organized properly.

PlanetIrata commented 8 years ago

Thank's kwoon and arqex for your answers, I'll give the live option a try arqex, but I have another problem with Freezer/React, that you may help me to solve : in your jsbin sample above, you store the currently selected Div as an array index in the store. It's ok for the demonstration, but how would you keep a trace of the selected Div with a much more complex structure like a tree of deeply nested Div components ? Suppose that we have Div in Div in Div, and that any of them should be selectable (and show it is selected with an outline like in your sample), and have its text editable in the same unique <input> field.

A possible solution would be to give each Div in the tree a guid, then I could store the selected Div guid in the store instead of an index, but I don't find this solution very elegant : firstly because I'd have to generate all those guid, and secondly because I'd have to pass 2 props to every Div in the tree : the selected property calculated by it's parent/container like in your sample, but also the guid of the selected Div in order for each Div to calculate the selected property for its children...

It would take 2 lines of jQuery to do the same (remove the "selected" state from all the components of a tree, and add the "selected" state to one of them) and I can't figure out how to do that in React with an immutable store to manage the app state...all the samples I see about this are for a 1 level relationship between parent <> children (active button in a menu bar, active tab...).

Hope you could help, anyway, thank you again for your great work !

arqex commented 8 years ago

Hey @PlanetIrata

Sorry but I missed your last comment here completely!

What you are asking is the one of the most strongest point of freezer. Using freezer it's really easy to handle infinitely deep data, because you can update a node data using the methods given by the own node. See the JSON object editor:

http://jsbin.com/hugusi/1/edit?js,output

There, a React component is created for every node inside the JSON object. If you want to edit some part of it, you never care about how deep you are inside of the data tree, you just do something like

this.props.data.set({b:3});

That would re-render the whole editor with the new data, no need of generating guid or something similar.

Using react you need to stop thinking about how to change the UI to update the data (jQuery way), and start thinking the other way around. How can I change the data to update the UI?

For your "selected" problem you would probably need something like this in your freezer store:

var freezer = new Freezer({
  data: {object1: {}, object2:{})
});
freezer.get().set({selected: freezer.get().data.object1});

You are storing what is the selected node separately, then in your component:

var className = 'component';
if( this.props.data === freezer.get().selected )
  className += ' selected';

That would select your component, and deselect any other.

I hope that helped, even if the reply was really late.

Cheers

arqex commented 8 years ago

Here you have the explanation for the JSON editor: http://arqex.com/991/json-editor-react-immutable-data

PlanetIrata commented 8 years ago

Thanks arqex, but in your sample, what happens if the selected node is modified from elsewhere :

// Your sample
var freezer = new Freezer({
  data: {object1: {}, object2:{})
});
freezer.get().set({selected: freezer.get().data.object1});

// Suppose now that somewhere in the code, object1 is modified
// (then its node is recreated)
freezer.get().data.object1.set({color: 'red'});

// From here, does freezer.get().selected is always a valid reference 
// on the new muted object1 node, or a bad reference to the old node ?
// If freezer.get().selected is invalid it would mean that editing the selected object
// would "unselect it" immediately

Thanks

arqex commented 8 years ago

Freezer is clever enough to update all the references to the same object inside the store, so freezer.get().selected will also be updated. freezer.get().selected === freezer.get().data.object1.

BerndWessels commented 8 years ago

@arqex Hi, your last comment, is that really true? I try this:

  setTimeout(() => {
    let state = this.props.store.get();
    state.app.human.set(0, {firstName: 'Bernd'});
    state.app.dog.set(99, {name: 'Brutus'});

    setTimeout(() => {
      let state = this.props.store.get();
      state.app.human.set(0, Object.assign({}, {lastName: 'Wessels', dog: state.app.dog[99]}, state.app.human[0]));

      setTimeout(() => {
        let state = this.props.store.get();
        state.app.dog.set(99, Object.assign({}, {age: 88}, state.app.dog[99]));

      }, 1000);

    }, 1000);

  }, 1000);
}

but at the end

state.app.human[0] is only {"lastName":"Wessels","dog":{"name":"Brutus"},"firstName":"Bernd"}

even though

state.app.dog[99] is {"age":88,"name":"Brutus"}

Basically what I am asking is - how can I make sure that object references are preserved / distributed in freezer?

arqex commented 7 years ago

Freezer is made to preserve references: http://jsbin.com/nahejorufo/1/edit?js,console