Redux Store & third party data structures - javascript

This is a hacky-ish question about mixing redux with 3rd party libraries. I'm well aware that this is contrary to the Redux guides. It's mostly for discussion and exploration - I also didn't want to pollute their Github issues ;)
The third party data
We're using a relational and immutable data structure module to store our application data. In a nutshell, the module works a little like a relational database:
It exposes Tables
Tables contain the application data
Tables have 4 methods: get, post, put, delete
Data objects dynamically reference each other by index/primary key
Current Redux Store structure
Since we're using Redux, we initially chose NOT to expose the tables directly, since they contain methods. We instead expose the result of Table.get();, which returns an array of it's data objects.
So our reducers "update state" by working with the 3rd party module. The heavy-lifting is done by the 3rd party, the reducers basically always return Table.get();.
Our store looks something like this:
// the application data (built by relational-json)
var appDB = {
People: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Organization: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Address: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
}
};
// example Reducer
store = createStore(combineReducers({
Person: function personReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_PERSON_LIST":
case "UPDATE_PERSON": {
appDB.Person.put(action.data, "Person");
return appDB.Person.get();
}
case "CREATE_PERSON": {
appDB.Person.post(action.data, "Person");
return appDB.Person.get();
}
default: {
return appDB.Person.get();
}
}
},
Organization: function personReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_ADDRESS_LIST":
case "UPDATE_ADDRESS": {
appDB.Organization.put(action.data, "Organization");
return appDB.Organization.get();
}
case "CREATE_ADDRESS": {
appDB.Organization.post(action.data, "Organization");
return appDB.Organization.get();
}
default: {
return appDB.Organization.get();
}
}
},
Address: function personReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_ADDRESS_LIST":
case "UPDATE_ADDRESS": {
appDB.Address.put(action.data, "Address");
return appDB.Address.get();
}
case "CREATE_ADDRESS": {
appDB.Address.post(action.data, "Address");
return appDB.Address.get();
}
default: {
return appDB.Address.get();
}
}
}
}));
// resulting initial state looks like:
var state = {
Person: [],
Organization: [],
Address: []
};
Our actual setup looks like above, but with close to 100 reducers. Most of the reducers are extremely identical, too. Their only changes are often the type of the action and the Table to update.
THE QUESTION
Alternative Store structure?
We're contemplating having a single reducer to handle the 3rd party data, and exposing the "Tables".get() in the Store structure. Our store would then have a much simpler structure (and much fewer reducers), and look something like:
// the application data (built by relational-json)
var appDB = {
People: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Organization: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Address: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
}
};
// example Reducer
store = createStore(combineReducers({
appDB: function dbReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_PERSON_LIST":
case "UPDATE_PERSON": {
appDB.Person.put(action.data, "Person");
break;
}
case "CREATE_PERSON": {
appDB.Person.post(action.data, "Person");
break;
}
case "UPDATE_ORGANIZATION_LIST":
case "UPDATE_ORGANIZATION": {
appDB.Organization.put(action.data, "Organization");
break;
}
case "CREATE_ORGANIZATION": {
appDB.Organization.post(action.data, "Organization");
break;
}
case "UPDATE_ADDRESS_LIST":
case "UPDATE_ADDRESS": {
appDB.Address.put(action.data, "Address");
break;
}
case "CREATE_ADDRESS": {
appDB.Address.post(action.data, "Address");
break;
}
default: {
break;
}
}
return Object.keys(appDB).reduce(function(obj, key) {
obj[key] = appDB[key].get;
return obj;
}, {})
}
}));
// resulting initial state looks like:
var state = {
appDB: {
People: function get() {},
Organization: function get() {},
Address: function get() {}
}
};
Where things conflict is that apis.apiX would be an object of methods mentioned above (get). The data isn't directly exposed, it has to be exposed by using Table.get();
This doesn't cause a problem in our case since we get data by using selectors (reselect), and they know when the data has changed, even if they have to go through Table.get();
Where I'm uneasy/unsure is for anything else expected or done by Redux. Would such a structure break things?

Both of these options are opposite to how Redux apps work because you rely on data mutations rather than return new objects.
While technically you can mutate data with Redux I think it's more trouble than it's worth in this scenario because you don't really get benefits of Redux. For example, the views can't redraw efficiently because mutations make it impossible to tell from a glance which data has changed.
This is why I suggest you to not use Redux if you want to treat a specific client-side DB as the source of truth. Redux only works well if it is the source of truth. So just use the DB directly and/or build an abstraction around it that better fits what you are trying to do.

From what I understand, Redux doesn't care what your store looks like. It's an abstraction. You decide how it looks. You also decide how actions affect it with reducers. Deciding to expose methods and/or data is irrelevant honestly. Nothing will break as long as you stick with your design decisions. The only thing I would be wary of is whether or not your application state is truly immutable by exposing methods rather than only data.
Besides that, knowing that your application state is reflected by third-party methods implies you develop components with that in mind. It may not be the agreed upon ideal example, but that doesn't mean you can't do it. That's my opinion anyway.

You may want to look into redux-orm. It provides a similar "model"-like API around Redux-style immutable reducers. Still new, but seems pretty useful so far.

Related

Reducers for dynamically created items

Is there any particular "good practice" in Redux when it comes to reducing state of dynamically created items? In this particular case, I'm dealing with a list of users that may join/leave the app, tables and games at any time.
let userReducer = (user, action) => {
switch(action.type) {
case 'table:create':
case 'table:join': return {
...user,
tables: [...user.tables, action.tableId]
}
case 'table:leave': return {
...user,
tables: user.tables.filter(tableId => tableId != action.tableId)
};
case 'game:join': return {
...user,
games: [...user.games, action.gameId]
};
case 'game:leave': return {
...user,
games: user.games.filter(gameId => gameId != action.gameId)
};
}
}
let usersById = (users = {}, action) => {
let user = users[action.userId];
switch(action.type) {
case 'user:join': return {
...users,
[action.user.id]: action.user
};
case 'user:leave': {
users = {...users};
delete users[action.userId];
return users;
};
case 'table:create':
case 'table:join':
case 'table:leave':
case 'game:join':
case 'game:leave': return {
...users,
[action.userId]: userReducer(user, action)
};
}
return users;
}
The last five cases in the second function's switch statement look particularly ugly to me. Maybe I could just condense it with an if? (if user is defined, then apply userReducer to it).
let usersById = (users = {}, action) => {
let user = users[action.userId];
if(user)
return {
...users,
[user.id]: userReducer(user, action);
}
switch(action.type) {
case 'user:join': return {
...users,
[action.user.id]: action.user
};
case 'user:leave': {
users = {...users};
delete users[action.userId];
return users;
};
}
return users;
}
I don't think there is any good practice in order to create reducers.
Personally I rather use the approach of your first exemple, as it make your code more readable. In addition it will allow you to keep the same structure to all your reducers.
On the contrary, that looks like some fairly well-organized reducer logic. But yes, if you want to use an if statement like that, you absolutely can - per the Redux FAQ on using switch statements, it's fine to use whatever logic approach you want in a reducer.
For more information on ways to organize reducer logic, see the Redux docs section on "Structuring Reducers", and my recent blog post Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy

Avoiding repeating state names

Let's say i have a rootreducer like below.
const rootR = combineReducers({
topics,
...
});
the topics reducer
function topics(state = { topics=[], error: null}, action){
switch (action.type){
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
topics,
};
default:
return state;
}
};
And when i fire the related action i get my state with repeatable properties state.topics.topics instead of state.topics
Is there any way to avoid this repeating (topics.topics)?
Thanks in advance
Looking at the initialState of your topics reducer, the state object accessible to topics reducer has this structure:
{
topics: [],
error: null
}
So when you combineReducers like this:
const rootR = combineReducers({
topics,
anotherReducer,
someOtherReducer.
// ...
});
resulting global app state is going to look like this:
{
topics: {
topics: [],
error: null
},
anotherReducer: {
// ...
},
someOtherReducer: {
// ...
},
// ...
}
So if you want to access topics array from global state, you need to do state.topics.topics.
You have two things under state.topics, an array of topics and error.
Hence let's rename second topics key to items to avoid confusion.
(it is unavoidable to have a second key to store the array because you also want error)
thus we have:
state.topics = {
items: [],
error: null,
}
Instead of state.topics.topics, now we access state.topics.items
To achieve this, initialstate passed to topics reducer has to be:
function topics(state = { items = [], error: null }, action){
//...
}
Now inside the reducer FETCH_TOPICS_SUCCESSFULLY, we want to append an array action.topics to items, like this (without mutating our current state):
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
items: [
...state.items,
...topics
],
};
#markerikson is right, the state variable passed in the function is actually topics once FETCH_TOPICS_SUCCESSFULLY is called, so it's better to do return topics there.
But given your condition, instead of return {...state, topics} or return topics, you can also do return Object.assign({}, state, topics). This will create a new object with all properties from previous state and topics merged together.
You're double-nesting things. The topics reducer will only see the "topics" slice of state. So, instead of returning {...state, topics}, just do return topics.
update
Your edit to the question changes the situation considerably.
Originally, you had:
function topics(state = {}, action){
Now, you have:
function topics(state = { topics=[], error: null}, action){
I'll admit I'm a bit confused at this point as to what your desired state structure actually should be.
Looking at your original definition, it seemed like you were misunderstanding how combineReducers works, and redundantly trying to return a new object that contained a field/slice named "topics". Now, it looks like the root-level "topics" slice itself has a field named "topics" as well.
Are topics and error supposed to be at the root of your state tree? Or, are they both really supposed to be part of the top-level "topics" slice? If that's really what you want, then you've defined the state tree as needing to be topics.topics.
Also, to answer #free-soul: no, in the original example, return topics would not mutate state, because it's just returning whatever was in the action. Even if the action.topic field was literally the same array that used to be in the state, the result would just be a no-op.

Use React state to manage list of already instanced objects?

I want to check each collection I instance to see if another instance of the same type already exists so I can use the stored version instead of creating a new one, how can I use the react state to keep a list to check against?
Sample code:
export default React.createBackboneClass({
getInitialState() {
return {
data: [],
groups: {}
}
},
componentWillMount() {
this.setState({
data: this.props.collection
});
},
render() {
const gridGroups = this.state.data.map((model) => {
let gridGroupsCollection = null;
if(this.state.groups[model.get('id')]) {
gridGroupsCollection = this.state.groups[model.get('id')];
} else {
gridGroupsCollection = this.state.groups[model.get('id')] = new GridGroupCollection([], {
groupId: model.get('id')
});
this.setState((previous) => {
groups: _.extend({}, previous, gridGroupsCollection)
});
}
return <GridGroupComponent
key={model.get('id')}
name={model.get('n')}
collection={gridGroupsCollection} />
});
return (
<div>
{gridGroups}
</div>
);
}
});
Few points. First of all, I would use the componentWillReviewProps to do the heavy work. Doing so on the render method can be more costly. I have yet to save instances on the state object. Is it needed for caching performance-wise or to solve a sorting issue?
Moreover, as noted already, calling setState from the render method will generate an infinite loop. as render is called when the state changes.
On a side note I would consider Redux for this, which offers several holistic ways to separate components from data manipulation.
update
This TodoMVC example might point you to the direction of how to combine react and backbone:
componentDidMount: function () {
// Whenever there may be a change in the Backbone data, trigger a
// reconcile.
this.getBackboneCollections().forEach(function (collection) {
// explicitly bind `null` to `forceUpdate`, as it demands a callback and
// React validates that it's a function. `collection` events passes
// additional arguments that are not functions
collection.on('add remove change', this.forceUpdate.bind(this, null));
}, this);
},
componentWillUnmount: function () {
// Ensure that we clean up any dangling references when the component is
// destroyed.
this.getBackboneCollections().forEach(function (collection) {
collection.off(null, null, this);
}, this);
}

redux how to get or store computed values

How should I implement in redux computed values based on it's current state?
I have this for an example a sessionState
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
}
}
export default function sessionReducer(state = defaultState, action) {
switch(action.type) {
case STORE_DATA:
return _.merge({}, state, action.data);
case PURGE_DATA:
return defaultState;
default:
return state;
}
}
Say for example I want to get if the session is logged in, I usually do right now is sessionState.data.id to check it, but I want to know how I can do sessionState.loggedIn or something?
Can this do?
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
},
loggedIn: () => {
this.data.id
}
}
or something (that's not tested, just threw that in). In this example it looks simple to just write .data.id but maybe when it comes to computations already, it's not good to write the same computations on different files.
Adding methods to state object is a bad idea. State should be plain objects. Keep in mind, that some libraries serialize the app state on every mutation.
You can create computing functions outside the state object. They can receive state as an argument. Why should they be state's methods?
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
}
}
const loggedIn = (state) => {
//your logic here
}
In your particular example, the calculated result is incredibly simple and fast to calculate so you can calculate it on demand each time. This makes it easy to define a function somewhere to calculate it like #Lazarev suggested.
However, if your calculations become more complicated and time consuming, you'll want to store the result somewhere so you can reuse it. Putting this data in the state is not a good idea because it denormalizes the state.
Luckily, since the state is immutable, you can write a simple pure function to calculate a result and then you can use memoization to cache the result:
const isLoggedIn = memoize(state => state.login.userName && state.login.token && isValidToken (state.login.token));
Lastly, if you want to use methods on your store state, you can use Redux Schema (disclaimer: I wrote it):
const storeType = reduxSchema.type({
ui: {
loading: Boolean
}, metadata: { },
data: {
id: reduxSchema.optional(String)
},
loggedIn() {
return Boolean(this.data.id);
}
});

Pattern for updating multiple parts of Redux state

The shape of my Redux state looks like this:
{
user: {
id: 123,
items: [1, 2]
},
items: {
1: {
...
},
2: {
...
}
}
}
Using combineReducers I have 2 sets of reducers. Each act on one of the root keys of the state. i.e. one manages the user key and the other the items key.
If I want to add an item I can call 2 reducers, the first will add a new object to the items and the second will add the id to the user.items array.
This has a bad code smell. I feel that there should be a way to atomically reduce the state of both objects at the same time. i.e. in addition to the sub-reducers have a root reducer that acts on the root object. Is this possible?
I think what you're doing is actually correct!
When dispatching an action, starting from the root-reducer, every "sub-reducer" will be called, passing the corresponding "sub-state" and action to the next layer of sub-reducers. You might think that this is not a good pattern since every "sub-reducer" gets called and propagates all the way down to every single leaf node of the state tree, but this is actually not the case!
If the action is defined in the switch case, the "sub-reducer" will only change the "sub-state" part it owns, and maybe passes the action to the next layer, but if the action isn't defined in the "sub-reducer", it will do nothing and return the current "sub-state", which stops the propagation.
Let's see an example with a more complex state tree!
Say you use redux-simple-router, and I extended your case to be more complex (having data of multiple users), then your state tree might look something like this:
{
currentUser: {
loggedIn: true,
id: 123,
},
entities: {
users: {
123: {
id: 123,
items: [1, 2]
},
456: {
id: 456,
items: [...]
}
},
items: {
1: {
...
},
2: {
...
}
}
},
routing: {
changeId: 3,
path: "/",
state: undefined,
replace:false
}
}
As you can see already, there are nested layers in the state tree, and to deal with this we use reducer composition, and the concept is to use combineReducer() for every layer in the state tree.
So your reducer should look something like this:
(To illustrate the layer by layer concept, this is outside-in, so the order is backwards)
first layer:
import { routeReducer } from 'redux-simple-router'
function currentUserReducer(state = {}, action) {
switch (action.type) {...}
}
const rootReducer = combineReducers({
currentUser: currentUserReducer,
entities: entitiesReducer, // from the second layer
routing: routeReducer // from 'redux-simple-router'
})
second layer (the entities part):
function usersReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
case TYPE_TWO:
case TYPE_TREE:
return Object.assign({}, state, {
// you can think of this as passing it to the "third layer"
[action.userId]: itemsInUserReducer(state[action.userId], action)
})
case TYPE_FOUR:
return ...
default:
return state
}
}
function itemsReducer(...) {...}
const entitiesReducer = combineReducers({
users: usersReducer,
items: itemsReducer
})
third layer (entities.users.items):
/**
* Note: only ADD_ITEM, TYPE_TWO, TYPE_TREE will be called here,
* no other types will propagate to this reducer
*/
function itemsInUserReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
return Object.assign({}, state, {
items: state.items.concat([action.itemId])
// or items: [...state.items, action.itemId]
})
case TYPE_TWO:
return DO_SOMETHING
case TYPE_TREE:
return DO_SOMETHING_ELSE
default:
state:
}
}
when an action dispatches
redux will call every sub-reducer from the rootReducer,
passing:
currentUser: {...} sub-state and the whole action to currentUserReducer
entities: {users: {...}, items: {...}} and action to entitiesReducer
routing: {...} and action to routeReducer
and...
entitiesReducer will pass users: {...} and action to usersReducer,
and items: {...} and action to itemsReducer
why is this good?
So you mentioned is there a way to have the root reducer handling different parts of the state, instead of passing them to separate sub-reducers. But if you don't use reducer composition and write a huge reducer to handle every part of the state, or you simply nest you state into a deeply nested tree, then as your app gets more complicated (say every user has a [friends] array, or items can have [tags], etc), it will be insanely complicated if not impossible to figure out every case.
Furthermore, splitting reducers makes your app extremely flexible, you just have to add any case TYPE_NAME to a reducer to react to that action (as long as your parent reducer passes it down).
For example if you want to track if the user visits some route, just add the case UPDATE_PATH to your reducer switch!

Categories

Resources