4
votes

[React-Redux] Issue.

I'd like to have reusable encapsulated components to be used in any app, or in any level of the app's store.

When it comes to use 'mapStatetoProps' then making the component container (injecting the state into the component as props), you always receive the whole store. This might be a pain if you want to reuse components dynamically or in other projects.

The thing is if you use the same store entry but you want to use the same component as encapsulated module they will be sharing the same data.

And also, when you are encapsulating components and you reuse them and they are deep nested in the store, you will end up needing to know where they are.

A possible ugly solution would be to implement a script going through the state inside the mapStateToProps till it finds the key matching certain name. The issue here would be to make sure the state field you want to use is unique.

I'll be more than happy to know any proper solution to this problem in an elegant way.

Or maybe we are just thinking the wrong way when talking about react-redux-sagas apps.

4

4 Answers

3
votes

For the sake of the example, I'll be talking about a reusable editor component, that edits documents and submits them to server.

In the code that is using the editor, I give each editor a unique id. E.g.

const Comment = (props) => {
  return <div>
    <h3>Add new comment</h3>
    <Editor editorId={`new-comment-${props.commentId}`} />
  </div>
}

In the redux state, I have one subreducer editor with objects keyed by the editorId, so the redux state is something like:

{
  otherStuff: {...},
  editor: {
    "new-comment-123": { isActive: true, text: "Hello there" },
    "new-comment-456": { isActive: false, text: "" },
    ...
  },
  ...
}

Then in Editor mapStateToProps I use selectors to get the data for the correct instance:

const mapStateToProps = (state, ownProps) => {
  return {
    isActive: selectors.isActive(state, ownProps.editorId),
    text: selectors.text(state, ownProps.editorId)
  }
}

The selectors are built in reselect style, either manually or by actually using reselect. Example:

// Manual code
export const getEditor = (state, editorId) => state.editor[editorId] || {};
export const isActive = (state, editorId) => getEditor(state, editorId).
export const text = (state, editorId) => getEditor(state, editorId).text;

// Same in reselect
import { createSelector } from 'reselect'

export const getEditor = (state, editorId) => state.editor[editorId] || {};
export const isActive = createSelector([getEditor], (editorData) => editorData.isActive);
export const text = createSelector([getEditor], (editorData) => editorData.text);

If you want to extend this to be used in multiple apps, you need to export your component, reducer and sagas. For a working example, check out https://github.com/woltapp/redux-autoloader or even http://redux-form.com

0
votes

If I understand your concern correctly, you could implement mapStateToProps as if it receives the part of state you need and call it, say, mapStateToYourComponentProps, and in actual mapStateToProps you just call mapStateToYourComponentProps and pass it appropriate part of state

0
votes

I found a way to make the components totally independent from state and hierarchy within the app.

Basically, each component must expose a method to set the path within the state. Then you have to initialize it when either when you import it before using it. You could also implement it in another way so you receive it inline as a prop.

It makes uses of reselect to establish the selection.

Each component knows the name of its key in the state.

The root component will import other components and it will call the setPath method of each one passing the root component path.

Then each component will call the setPath of each subcomponent passing their own location in the state. "Each parent will init their children"

So each component will set a path in the store based on the naming "parent path + local path (component key name in the store)".

This way you would be defining a nested routing with 'createSelector' method from reselect, like this: ['rootKey','subComponent1Key','subsubComponent1Key].

With this, you have the store isolation completed. Redux actions will just change the bit needed so yo have this part also covered by the framework.

It worked like a charm for me, please let me know if its good before I mark it as good.

0
votes

If you have some free time, try the npm package redux-livequery (https://www.npmjs.com/package/redux-livequery) I just wrote recently.

There is another way to manage your active list.

    let selector0 = (state) => state.task.isComplete;
    let selector1 = (state) => state.task.taskList;
    this.unsub2 = rxQueryBasedOnObjectKeys([selector0, selector1], ['isActive', 'task'], (completeTaskList) => {
        // equal SQL =>
        // select * from isActive LEFT JOIN taskList on isActive.child_key == taskList.child_key
        console.log("got latest completeTaskList", completeTaskList);
        // you can do whatever you want here
        // ex: filter, reduce, map

        this.setState({ completeTaskList });
    }, 0);

In the reducer:

    case "MARK_ACTIVE_TASK": {
        let { id } = action.meta;
        return update(state, { isActive: { [id]: { $set: { active: Date.now() } } } });
    }
    case "UNMARK_ACTIVE_TASK": {
        let { id } = action.meta;
        return update(state, { isActive: { $apply: function (x) { let y = Object.assign({}, x); delete y[id]; return y; } } });
    }

It lets you have simpler reducer. In addition, there is no more nested selector function or filter which is really expensive operation. Putting your all logic in the same place would be great.

And it can do even more complexity operation like how to get complete and active list.

    let selector0 = (state) => state.task.isComplete;
    let selector1 = (state) => state.task.isActive;
    let selector2 = (state) => state.task.taskList;
    this.unsub3 = rxQueryInnerJoin([selector0, selector1, selector2], ['isComplete', 'isActive', 'task'], (completeAndActiveTaskList) => {
        // equal SQL =>
        // select * from isComplete INNER JOIN isActive on isComplete.child_key == isActive.child_key
        //                          INNER JOIN taskList on isActive.child_key == taskList.child_key
        console.log("got latest completeAndActiveTaskList", completeAndActiveTaskList);
        // you can do whatever you want here
        // ex: filter, reduce, map

        this.setState({ completeAndActiveTaskList });
    }, 0);

If you would like to get complete or active list, it's also easy to get. The more example, please refer to the sample code => https://github.com/jeffnian88/redux-livequery-todos-example