I am working on a music application that is split between several features:
library: displays the available content and allows the user to browse artists and albums, and play one or a few tracks. The state looks like this:
library {
tracksById: {...}
albumsById: {...}
artistsById: {...}
albums: []
artists: []
}
player: the part that plays the music. Just contains an array of all tracks to be played. Also shows a list of the tracks to be played
and etc; The problem is that I would like to share tracksById, albumsById and artistsById between all features, because each album has an artist property, but this property is just an id, so if I want to display the artist name near the track name in the playlist, I need to fetch the artist, and put it in my store. I could simply connect them both like this:
#connect(createStructuredSelector({
player: (state) => state.player,
library: (state:) => state.library
}), (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
}))
And then in my playlist view:
<div className="artist-name">{this.library.artistsById[track.artist].name}</div>
However this increases coupling between features and I find that it defeats the very purposes of multiple reducers.
Another way would be to create a new feature content, which would only contain the actions and byId properties needed with the most minimal reducer possible and this one would be shared by other features. However, this means I have to add a new prop to every component content, and I need to add the actions.
I could also create a service, or even maybe a function that would take a a reducer and actions object, and add a logic to make it able to fetch artists, albums and tracks and save them in the store. I really like this idea since it decreases coupling, however, it means duplicated data in my state, which means more memory used.
I am not looking for the best way, just wondering if there is any other and what are the pros and cons of each method
I'm pretty sure I don't fully understand your question, but I'll share some thoughts. Are you trying to avoid duplicate queries or duplicated state data?
Objects are fast dictionaries
This bit of duplication:
{this.library.artistsById[track.artist].name}
becomes unnecessary if library.artists is an object instead of an array. Assume 1 and 2 are artist IDs, artists would use the artist ids as keys in the object:
{
1: {name: 'Bob Marley', ...etc},
2: {name: 'Damien Marley', ...etc}
}
Which lets you get artist info from library.artists using only the id:
{this.library.artists[track.artistId].name}
Use selector functions
But what you really want is getArtistById(state, artistId){}, a selector function in your reducer code file. That lets you rearrange the state any time and none of your view components will be affected. If artists is an object with the keys being artist IDs, the implementation could look like this:
getArtistById(state, artistId){
return state.library.artists[artistId]
}
I agree with your observation that accessing library.artistsById[track.artist].name adds unnecessary coupling. I would never put code that knows about high-level state shape into a view component.
For your example playlist view with a list of artists, each item in the playlist view would be its own tiny component. The container would look like this:
import { getArtistById } from '../reducers/root-reducer'
const mapStateToProps = (state) => {
const artist = getArtistById(state.library)
return { artistName: artist ? artist.name : '' }
}
...
The tiny view component would look something like:
render() {
const {artistName} = this.props
return <div className="artist-name">{artistName}</div>
}
Premature optimization?
You might be prematurely optimizing. Even if you keep artists and albums as arrays, your selector function can search through the array for the ID. A function to get each of those by id from a state that only contains { artists: [], albums: [], tracks:[] } should be very fast until the library got pretty huge.
Related
I'm using immer (via createSlice in redux-toolkit) to manage state in a redux application. I've written a reducer that takes a payload of music tracks and adds them to the users library. In part, this reducer must also create artist entries for each of the tracks.
createTracks(draft, action: PayloadAction<{ tracks: Array<ITrack> | ITrack; }>) {
...
tracks.forEach((track, index) => {
...
// Check to see if artist exists, if not create, if so, add this track
let artist = draft.artists[artistId];
if (!artist) {
console.log("creating artist", track.artist);
draft.artists[artistId] = produce({
name: track.artist,
albums: [],
id: artistId,
tracks: [track.id]
}, (artistDraft): IArtist => {
console.log("producing the artist");
return artistDraft;
});
} else {
console.log("updating artist");
draft.artists[artistId].tracks.push(track.id);
}
This works fine for the first tracks from each artist. However, when immer creates each new artist object and adds it to the state, it makes the track array for each object not extensible.
Uncaught TypeError: Cannot add property 1, object is not extensible
is thrown from
draft.artists[artistId].tracks.push(track.id);
I've presumed that this is because I'm either not creating the the nested artist draft correctly or redux doesn't support this behavior. That's why I shifted from using a pure object to a nested immer draft - but this didn't seem to work.
I have also tried...
draft.artists[artistId].tracks = [...draft.artists[artistId].tracks, track.id];
... but in this case, the tracks property is read only.
How can I implement this without the error?
I finally traced this down. My reducers were written correctly, but higher up the stack I was accidentally importing combineReducers from redux-immer instead of redux itself. redux-immer is a library for integrating immer into redux.
This resulted in running my store through immer twice (once via createSlice and once via combineReducers), which caused... unexpected... behaviors, including immer locking itself, resulting in the objects described above getting locked.
It's been a fun way to spend the last two weeks, for sure.
I have an async api call where I get an array of objects and then I map that to dynamically registered modules in my store. Something like this:
dispatch
// before this dispatch some api call happens and inside the promise
// iterate over the array of data and dispatch this action
dispatch(`list/${doctor.id}/availabilities/load`, doctor.availabilities);
The list/${doctor.id} is the dynamic module
action in availabilities module
load({ commit }, availabilities) {
const payload = {
id: availabilities.id,
firstAvailable: availabilities.firstAvailable,
timeslots: [],
};
// then a bunch of code that maps the availabilities to a specific format changing the value of payload.timeslots
commit('SET_AVAILABILITIES', payload)
}
mutation
[types.SET_TIMESLOTS](state, payload) {
console.log(payload);
state.firstAvailable = payload.firstAvailable;
state.id = payload.id;
state.timeslots = payload.timeslots;
}
When I check my logs for the console.log above each doctor has different arrays of time slots Exactly the data I want. However, in the vue developer tools and what is being rendered is just the last doctor's timeslots for all of the doctors. All of my business logic is happening in the load action and the payload in the mutation is the correct data post business logic. Anyone have any ideas why I'm seeing the last doctor's availabilities for every doctor?
It looks like you are assigning the same array (timeslots) to all doctors.
When you add an element to the array for one doctor, you mutate the array that all doctors are sharing.
However with the little code you show, it's difficult to know where is the exact problem.
I have a To-Do List app that creates a TodoItem by dispatching a CREATE_TODO_REQUEST action, which causes a middleware to make aPOST request to an API and respond with CREATE_TODO_SUCCESS with the newly created TodoItem returned by the API. This ToDoItem has a messy hexadecimal ID (like 59e52a5ec8dae14f2420a9ef) assigned to it by our database.
The problem is, sometimes the API could take a few seconds to respond (especially if the user is on a weak connection), so I'd want to optimistically update our application state with the new ToDoItem before the server is done processing it.
This pattern gets messy because all my TodoItems are indexed by ID in my Redux store, and their order is stored in a list of IDs. These IDs are generated by the API after a ToDoItem gets created.
{
byId: {
59e52a5ec8dae14f2420a9ef: {...},
59e52a5ec8dae14f2420a434: {...}
},
ids: [
'59e52a5ec8dae14f2420a9ef',
'59e52a5ec8dae14f2420a434'
]
}
My question is, what ID should I assign my eagerly-created ToDoItem while I wait for the API to return the newly created ToDoItem with a proper ID? Is there an established pattern for handling this type of situation?
I could use a random number generator to create a provisional ID and replace it with the real ID when the CREATE_TODO_SUCCESS action is dispatched (see sample app state below).
{
byId: {
59e52a5ec8dae14f2420a9ef: {...},
59e52a5ec8dae14f2420a434: {...},
"provisional-todo-1": {...} // this is being created on the API rn
},
ids: [
'59e52a5ec8dae14f2420a9ef',
'59e52a5ec8dae14f2420a434',
'provisional-todo-1'
]
}
But this might require some complex logic keeping track of which provisional ToDoItem is associated with actual ToDoItems that are later returned from the server. Additionally, there is the complexity associated with making sure actions dispatched against provisional ToDoItems (marking as complete, editing, deleting) are applied to the correct "real" ToDoItems after they are created.
The easiest answer is to create an local object with a mapping to the remote id.
For example, it might look something like this:
class Todo {
constructor() {
this.id = 'local' + Todo.globalId;
Todo.globalId += 1;
this.remoteId = null;
}
resolve(remoteId) {
this.remoteId = remoteId;
}
}
Todo.globalId = 0;
In redux, you could store these Todo objects, and use those internally to track your state. Then, when the API finally comes back with a value, you can set the remoteId. If there is some failure you could remove the local object or perhaps set a flag.
We have a list of lectures and chapters where the user can select and deselect them. The two lists are stored in a redux store.
Now we want to keep a representation of selected lecture slugs and chapter slugs in the hash tag of the url and any changes to the url should change the store too (two-way-syncing).
What would be the best solution using react-router or even react-router-redux?
We couldn't really find some good examples where the react router is only used to maintain the hash tag of an url and also only updates one component.
I think you don’t need to.
(Sorry for a dismissive answer but it’s the best solution in my experience.)
Store is the source of truth for your data. This is fine.
If you use React Router, let it be the source of truth for your URL state.
You don’t have to keep everything in the store.
For example, considering your use case:
Because the url parameters only contain the slugs of the lectures and the chapters which are selected. In the store I have a list of lectures and chapters with a name, slug and a selected Boolean value.
The problem is you’re duplicating the data. The data in the store (chapter.selected) is duplicated in the React Router state. One solution would be syncing them, but this quickly gets complex. Why not just let React Router be the source of truth for selected chapters?
Your store state would then look like (simplified):
{
// Might be paginated, kept inside a "book", etc:
visibleChapterSlugs: ['intro', 'wow', 'ending'],
// A simple ID dictionary:
chaptersBySlug: {
'intro': {
slug: 'intro',
title: 'Introduction'
},
'wow': {
slug: 'wow',
title: 'All the things'
},
'ending': {
slug: 'ending',
title: 'The End!'
}
}
}
That’s it! Don’t store selected there. Instead let React Router handle it. In your route handler, write something like
function ChapterList({ chapters }) {
return (
<div>
{chapters.map(chapter => <Chapter chapter={chapter} key={chapter.slug} />)}
</div>
)
}
const mapStateToProps = (state, ownProps) => {
// Use props injected by React Router:
const selectedSlugs = ownProps.params.selectedSlugs.split(';')
// Use both state and this information to generate final props:
const chapters = state.visibleChapterSlugs.map(slug => {
return Object.assign({
isSelected: selectedSlugs.indexOf(slug) > -1,
}, state.chaptersBySlug[slug])
})
return { chapters }
}
export default connect(mapStateToProps)(ChapterList)
react-router-redux can help you inject the url stuff to store, so every time hash tag changed, store also.
I'm rewriting my app to use Flux and I have an issue with retrieving data from Stores. I have a lot of components, and they nest a lot. Some of them are large (Article), some are small and simple (UserAvatar, UserLink).
I've been struggling with where in component hierarchy I should read data from Stores.
I tried two extreme approaches, neither of which I quite liked:
All entity components read their own data
Each component that needs some data from Store receives just entity ID and retrieves entity on its own.
For example, Article is passed articleId, UserAvatar and UserLink are passed userId.
This approach has several significant downsides (discussed under code sample).
var Article = React.createClass({
mixins: [createStoreMixin(ArticleStore)],
propTypes: {
articleId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
article: ArticleStore.get(this.props.articleId);
}
},
render() {
var article = this.state.article,
userId = article.userId;
return (
<div>
<UserLink userId={userId}>
<UserAvatar userId={userId} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink userId={userId} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<Link to='user' params={{ userId: this.props.userId }}>
{this.props.children || user.name}
</Link>
)
}
});
Downsides of this approach:
It's frustrating to have 100s components potentially subscribing to Stores;
It's hard to keep track of how data is updated and in what order because each component retrieves its data independently;
Even though you might already have an entity in state, you are forced to pass its ID to children, who will retrieve it again (or else break the consistency).
All data is read once at the top level and passed down to components
When I was tired of tracking down bugs, I tried to put all data retrieving at the top level. This, however, proved impossible because for some entities I have several levels of nesting.
For example:
A Category contains UserAvatars of people who contribute to that category;
An Article may have several Categorys.
Therefore if I wanted to retrieve all data from Stores at the level of an Article, I would need to:
Retrieve article from ArticleStore;
Retrieve all article's categories from CategoryStore;
Separately retrieve each category's contributors from UserStore;
Somehow pass all that data down to components.
Even more frustratingly, whenever I need a deeply nested entity, I would need to add code to each level of nesting to additionally pass it down.
Summing Up
Both approaches seem flawed. How do I solve this problem most elegantly?
My objectives:
Stores shouldn't have an insane number of subscribers. It's stupid for each UserLink to listen to UserStore if parent components already do that.
If parent component has retrieved some object from store (e.g. user), I don't want any nested components to have to fetch it again. I should be able to pass it via props.
I shouldn't have to fetch all entities (including relationships) at the top level because it would complicate adding or removing relationships. I don't want to introduce new props at all nesting levels each time a nested entity gets a new relationship (e.g. category gets a curator).
Most people start out by listening to the relevant stores in a controller-view component near the top of the hierarchy.
Later, when it seems like a lot of irrelevant props are getting passed down through the hierarchy to some deeply nested component, some people will decided it's a good idea to let a deeper component listen for changes in the stores. This offers a better encapsulation of the problem domain that this deeper branch of the component tree is about. There are good arguments to be made for doing this judiciously.
However, I prefer to always listen at the top and simply pass down all the data. I will sometimes even take the entire state of the store and pass it down through the hierarchy as a single object, and I will do this for multiple stores. So I would have a prop for the ArticleStore's state, and another for the UserStore's state, etc. I find that avoiding deeply nested controller-views maintains a singular entry point for the data, and unifies the data flow. Otherwise, I have multiple sources of data, and this can become difficult to debug.
Type checking is more difficult with this strategy, but you can set up a "shape", or type template, for the large-object-as-prop with React's PropTypes. See:
https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91
http://facebook.github.io/react/docs/reusable-components.html#prop-validation
Note that you may want to put the logic of associating data between stores in the stores themselves. So your ArticleStore might waitFor() the UserStore, and include the relevant Users with every Article record it provides through getArticles(). Doing this in your views sounds like pushing logic into the view layer, which is a practice you should avoid whenever possible.
You might also be tempted to use transferPropsTo(), and many people like doing this, but I prefer to keep everything explicit for readability and thus maintainability.
FWIW, my understanding is that David Nolen takes a similar approach with his Om framework (which is somewhat Flux-compatible) with a single entry point of data on the root node -- the equivalent in Flux would be to only have one controller-view listening to all stores. This is made efficient by using shouldComponentUpdate() and immutable data structures that can be compared by reference, with ===. For immutable data structures, checkout David's mori or Facebook's immutable-js. My limited knowledge of Om primarily comes from The Future of JavaScript MVC Frameworks
The approach at which I arrived is having each components receive its data (not IDs) as a prop. If some nested component needs a related entity, it's up to the parent component to retrieve it.
In our example, Article should have an article prop which is an object (presumably retrieved by ArticleList or ArticlePage).
Because Article also wants to render UserLink and UserAvatar for article's author, it will subscribe to UserStore and keep author: UserStore.get(article.authorId) in its state. It will then render UserLink and UserAvatar with this this.state.author. If they wish to pass it down further, they can. No child components will need to retrieve this user again.
To reiterate:
No component ever receives ID as a prop; all components receive their respective objects.
If child components needs an entity, it's parent's responsibility to retrieve it and pass as a prop.
This solves my problem quite nicely. Code example rewritten to use this approach:
var Article = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
article: PropTypes.object.isRequired
},
getStateFromStores() {
return {
author: UserStore.get(this.props.article.authorId);
}
},
render() {
var article = this.props.article,
author = this.state.author;
return (
<div>
<UserLink user={author}>
<UserAvatar user={author} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink user={author} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
propTypes: {
user: PropTypes.object.isRequired
},
render() {
var user = this.props.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
propTypes: {
user: PropTypes.object.isRequired
},
render() {
var user = this.props.user;
return (
<Link to='user' params={{ userId: this.props.user.id }}>
{this.props.children || user.name}
</Link>
)
}
});
This keeps innermost components stupid but doesn't force us to complicate the hell out of top level components.
My solution is much simpler. Every component that has its own state is allowed to talk and listen to stores. These are very controller-like components. Deeper nested components that don't maintain state but just render stuff aren't allowed. They only receive props for pure rendering, very view-like.
This way everything flows from stateful components into stateless components. Keeping the statefuls count low.
In your case, Article would be stateful and therefore talks to the stores and UserLink etc. would only render so it would receive article.user as prop.
The problems described in your 2 philosophies are common to any single page application.
They are discussed briefly in this video: https://www.youtube.com/watch?v=IrgHurBjQbg and Relay ( https://facebook.github.io/relay ) was developed by Facebook to overcome the tradeoff that you describe.
Relay's approach is very data centric. It is an answer to the question "How do I get just the needed data for each components in this view in one query to the server?" And at the same time Relay makes sure that you have little coupling across the code when a component used in multiple views.
If Relay is not an option, "All entity components read their own data" seems a better approach to me for the situation you describe.
I think the misconception in Flux is what a store is. The concept of store exist no to be the place where a model or a collection of objects are kept. Stores are temporary places where your application put the data before the view is rendered. The real reason they exist is to solve the problem of dependencies across the data that goes in different stores.
What Flux is not specifying is how a store relate to the concept of models and collection of objects (a la Backbone).
In that sense some people are actually making a flux store a place where to put collection of objects of a specific type that is not flush for the whole time the user keeps the browser open but, as I understand flux, that is not what a store is supposed to be.
The solution is to have another layer where you where the entities necessary to render your view (and potentially more) are stored and kept updated. If you this layer that abstract models and collections, it is not a problem if you the subcomponents have to query again to get their own data.