I'm struggling with the concept of normalizing the data in Redux.
For example, if we normalize the following data:
{
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [
{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}
]
}
to this:
{
result: "123",
entities: {
"articles": {
"123": {
id: "123",
author: "1",
title: "My awesome blog post",
comments: [ "324" ]
}
},
"users": {
"1": { "id": "1", "name": "Paul" },
"2": { "id": "2", "name": "Nicole" }
},
"comments": {
"324": { id: "324", "commenter": "2" }
}
}
}
And in my template, I need to loop over the posts and show the post.comments, for example in Angular -
<ul>
<li *ngFor="let post of posts">
{{post.title}}
<div *ngFor="let post of post.comments">...</div>
</li>
</ul>
I need to do some transformation to get back this structure. Otherwise, I can't display the data in my page, So what is the whole point?
The redux docs have a pretty detailed section on this topic but the main reasons are:
This is a concern for several reasons:
When a piece of data is duplicated in several places, it becomes harder to make sure that it is updated appropriately.
Nested data means that the corresponding reducer logic has to be more nested or more complex. In particular, trying to update a deeply
nested field can become very ugly very fast.
Since immutable data updates require all ancestors in the state tree to be copied and updated as well, and new object references will
cause connected UI components to re-render, an update to a deeply
nested data object could force totally unrelated UI components to
re-render even if the data they're displaying hasn't actually changed.
From personal experience, I can say that the third point can deliver significant performance improvements in some situations.
Since you asked, here's an example of how you might look up the Comment entries for a given Post:
const mapState = (state, ownProps) => {
let currentPost, postComments;
if(ownProps.postId) {
currentPost = state.posts[postId];
if(currentPost) {
const {comments : commentIds} = currentPost;
postComments = commentIds.map(commentId => state.comments[commentId]);
}
}
return {currentPost, postComments};
}
class Post extends React.Component { /* component code here */ }
export default connect(mapState)(Post);
This would then be used as <Post postId={123} />.
Related
I've got a simple JSON data array that looks like this (it's the same in my Redux store and I store it under properties):
[
{
"id": "room",
"name": "Room",
"description": "Just a room",
"foo": "bar",
},
{
"id": "apartment",
"name": "Apartment",
"description": "just an apartment",
"foo": "bar",
}
]
It has to be an array because I'm mapping over it:
{properties.map(property =>
<Property
id={property.id}
name={property.name}
description={property.description}
foo={property.foo}
/>)}
This way I have two properties rendered in my app.
The question is - how do I update "foo"in Redux?
This my reducer currently written according to the docs:
case 'UPDATE_FOO':
return {
...state,
properties: {
...state.properties,
[action.id]: {
...state.properties[action.id],
foo: action.fooData,
}
}
}
Of course it throws TypeError: Cannot read property 'room' of undefined because I'm trying to access state.properties["room"] and this does not exist.
I've been thinking about reshaping my JSON and Redux store, but once I change it to named objects I can't map over it...
Thank you!
Change your store to be an object, and generate the array, when you need to render it, by converting it to an array, using Object.values():
{Object.values(properties).map(property => (
<Property key={property.id} {...property} />
)}
If the order might change (sorting for example), you need to have another array with the ids to hold the order (see redux normalized state):
{
properties: {
room: {
"id": "room",
"name": "Room",
"description": "Just a room",
"foo": "bar",
},
apartment: {
"id": "apartment",
"name": "Apartment",
"description": "just an apartment",
"foo": "bar",
}
}
}
{
properties: {
room: {
"id": "room",
"name": "Room",
"description": "Just a room",
"foo": "bar",
},
apartment: {
"id": "apartment",
"name": "Apartment",
"description": "just an apartment",
"foo": "bar",
}
},
order: ['apartment', 'room']
}
{order.map(id => (
<Property key={id} {...properties[id]} />
)}
If you want to update the store using but the Id. You can look up for the one element (you're trying to edit) in array of properties using the id, then to update it:
case 'UPDATE_FOO':
const newProperties = state.properties.map(property => {
if(property.id === action.id) {
return { ...property, foo: action.fooData}
}
return property;
})
return {
...state,
properties: newProperties
}
}
I'm currently building software with Rails + Ember 3.12, but hitting a strange issue.
My models are the following:
// test-case-run
import DS from 'ember-data';
const { Model } = DS;
export default Model.extend({
testCase: DS.belongsTo('test-case'),
testCaseRunLogs: DS.hasMany('test-case-run-logs')
});
// test-case-run-log
import DS from 'ember-data';
const { Model } = DS;
export default Model.extend({
testCaseRun: DS.belongsTo('test-case-run'),
payload: DS.attr('')
});
And, my backend is returning the following payload:
{
"data": {
"id": "83",
"type": "test_case_run",
"relationships": {
"test_case": {
"data": {
"id": "90",
"type": "test_case"
}
},
"test_case_run_logs": {
"data": []
}
}
}
}
{
"data": {
"id": "83",
"type": "test_case_run",
"relationships": {
"test_case": {
"data": {
"id": "90",
"type": "test_case"
}
},
"test_case_run_logs": {
"data": [
{
"id": "426",
"type": "test_case_run_log"
}
]
}
}
},
"included": [
{
"id": "426",
"type": "test_case_run_log",
"attributes": {
"payload": "SCREENSHOT"
},
"relationships": {
"test_case_run": {
"data": {
"id": "83",
"type": "test_case_run"
}
}
}
}
]
}
I've got a custom adapter defining:
pathForType(type) {
return underscore(pluralize(type));
}
So, I think that everything should go well.
However, when I get into the ember inspector, I've got the following:
It seems that my relationship is not loaded properly.
And, I cannot access any data, such as:
log.get('testCaseRun') // that is null
run.get('testCaseRunLogs.length') // it returns 0
This is quite strange, as my records are loaded in the store, but not their relationships.
I have no idea on how to troubleshoot this, since the amount of information I can get from ember is quite limited (there is no error, the format looks good, ...).
Could someone help me to understand what's wrong with my calls? I've tried many things, such as renaming my models, but this does not improve the situation.
Moreover, this model is the only one, which I have problem with. All my other models don't have this problem. So, that's a bit weird.
Thanks a lot
The unknown in <(unknown):ember264> refers to the name of the class. That doesn't mean that your relationship is not loaded correctly. It's just Ember Data using anonymous classes.
To see the data of the relationship you could click on that string and afterwards on content. Another option is passing the full record to the console using the $E link in top right corner. Afterwards you could interact with the record on console, e.g. do a $E.get('testCaseRun.id').
By the way: You don't need to explicitly declare the model name on relationship definition if it's matches the dasherized property name. So testCaseRun: DS.belongsTo('test-case-run') is the same as testCaseRun: DS.belongsTo().
Try to declare hasMany relationship with the model name without 's'
testCaseRunLogs: DS.hasMany('test-case-run-log')
Finally, I've found the answer of my question.
The problem was that I was using the "underscore" form of the relationships:
"included": [
{
"id": "426",
"type": "test_case_run_log", <= HERE
"attributes": {
"payload": "SCREENSHOT"
},
"relationships": {
"test_case_run": {
"data": {
"id": "83",
"type": "test_case_run" <= HERE
}
}
}
}
]
And, changing pathForType was not sufficient.
So, I made my backend use dashes. And, it worked.
I tried all the day to find a solution on how I can access Facebook nested response.
I use FB.api with a nested request and I got the response and it is ok.
Here a sample of the Response I got from Facebook Graph Api
{
"insights": {
"data": [
{
"name": "page_fan_adds_unique",
"period": "month",
"values": [
{
"value": 272,
"end_time": "2019-10-08T07:00:00+0000"
},
{
"value": 270,
"end_time": "2019-10-09T07:00:00+0000"
}
],
"title": null,
"description": "The number of new people who have liked your Page.",
"id": "1021596371302281/insights/page_fan_adds_unique/month"
},
{
"name": "page_fan_removes_unique",
"period": "month",
"values": [
{
"value": 450,
"end_time": "2019-10-08T07:00:00+0000"
},
{
"value": 453,
"end_time": "2019-10-09T07:00:00+0000"
}
],
"title": null,
"description": "Unlikes of your Page.",
"id": "1021596371302281/insights/page_fan_removes_unique/month"
},
{
"name": "page_views_total",
"period": "month",
"values": [
{
"value": 6430,
"end_time": "2019-10-08T07:00:00+0000"
},
{
"value": 6339,
"end_time": "2019-10-09T07:00:00+0000"
}
],
"title": null,
"description": "The number of times a Page's profile has been viewed by logged in and logged out people.",
"id": "1021596371302281/insights/page_views_total/month"
}
],
i'm using react. I made the APi call in the parent component like so :
async componentDidMount() {
const resI = await axios.get(
'https://graph.facebook.com/MyID/',
{
params: {
access_token: process.env.REACT_APP_FACEBOOK_ACCESS_TOKEN,
fields:
'insights.metric(page_fans, post_engaged_users, post_impressions_unique, page_fan_adds_unique, page_fan_removes_unique, page_views_total, page_fans_gender_age, page_fans_country, page_fans_city, page_fans_locale).period(month)'
}
}
);
this.setState({insights: resI.data});
console.log(this.state.insights.insights.data[0].values[1].value); //I Get 270. It working in the parent component
}
I past the data I got from the API as a props to my other components
....
<div>
<GlobalPerf insights={this.state.insights} />
</div>
......
Here is where the problem start. I can't access value in the values array within my child that components
class GlobalPerf extends Component {
render() {
const ins = this.props.insights; // I can't go deeper than that.
console.log(ins.insights);
I can't access the value in the object. I can't go deeper than this.props.insights
When I try this.props.insights.data it not working. Can someone help me figuring out ? Thx
this.setState({insights: resI.data});
console.log(this.state.insights.insights.data[0].values[1].value); //I Get 270. It working in the parent component
The above statements should not work, since the setState is asynchronous and does not update the value synchronously. Probably showing the old state value?
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
You may be trying to access the props.insights before axios completes
const ins = this.props.insights; // probably wait for the axios to complete the fetch before trying to access the value! write it as
console.log(ins && ins.insights); //will give insights once the component rerenders after the props change due to the state being updated in the parent after axios returns!
I've done Dan Abramov's intro series on EggHead, and am working on a real world app. The domain is complex, so I'll run with a classic "blogs" example.
Let's say, we have an "index / list" page, where all we need to show are the blog post's title and blurb. So we have an API endpoint that returns that, and we store it in our state tree under blogs.byId.
Then, when you click through on a blog post, we actually need a bunch more info - e.g. the full blog post, and also tags and categories. Let's call this "blogs with metadata".
Stretching the example, there might be another completely separate page where I want to display a list of blog posts with the most recent 3 comments. Let's call this "blogs with comments".
My question is, how should my state tree treat these separate examples where I'm storing the same "thing" but in different "formats"? My initial hunch would be to treat them as completely separate data types, so my state tree would have eg: blogs.byId, blogsWithMetadata.byId and blogsWithComments.byId.
And then, even if every single blog post is cached in the blogs.byId section, the minute we need to view a blog post, the app completely ignores that warm blogs.byId cache, and looks only at blogsWithMetadata.byId - so we'd essentially be building up 3 separate caches of blog data, each with different amounts of info, and treating it as though they are as unrelated to each other as "blogs" and a completely unrelated table like "widgets" would be.
Is this correct? Or is there a better way?
The app currently rams them all under the same node, without distinction based on "format" and it's causing a world of pain.
There are probably many ways you could choose to do this. One of it is to use normalizr to structure your data.
Your blog post could have a data structure returned by the API like this:
{
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}],
"tags": [{
"id": "1",
"value": "awesome"
}, {
"id": "2",
"value": "journal"
}],
"categories": [{
"id": "1",
"value": "personal"
}, {
"id": "2",
"value": "life"
}]
}
which after normalizing, will look something like this:
{
entities: {
"post": {
"123": {
id: "123",
author: "1",
title: "My awesome blog post",
comments: ["324"],
tags: ["1", "2"],
categories: ["1", "2"],
}
},
"users": {
"1": { "id": "1", "name": "Paul" },
"2": { "id": "2", "name": "Nicole" }
},
"comments": {
"324": { id: "324", "commenter": "2" }
}
"tags": {
"1": { id: "1", "value": "awesome" },
"2": { id: "2", "value": "journal" },
}
"categories": {
"1": { id: "1", "value": "personal" },
"2": { id: "2", "value": "life" },
}
}
}
Subsequently, you could have a state for each page if you needed to:
{
entities: {...},
ui: {
blogs: {
posts: [1, 2],
hasComments: false,
// Displaying the blogs with or without comments
// could simply just be a boolean flag in state.
},
}
}
using reselect, you then create the selectors to pass the posts you want as props to the page Components.
I know there's plenty of answers on this and most are suggesting looping thru the object, returning what you need, I'm not sure this is the best approach, in my case anyway.
What I have is array with entries referencing to another array people(with id and name) by person_id and projects(with id and name) by project_id.
What I need is to be able to access project and person with a particular id inside the loop on entries, so I can get their names. Doing what others have suggested I'd loop thru people and projects inside each irritation of entries, which seems like awful lot of looping.
So I thought I'd make something I called a "hashtable" from both people and projects on init, which means pretty much creating a new objects people_hashtable and projects_hashtable where key would be the id
so
[
{
"id": "8",
"name": "John Doe"
}
]
would became
{
"8": {
"name": "John Doe"
}
}
this way I'd have easy access to the name without looping all the time while still maintaining the old array with its original order(that's why I'm not outputting it this way directly from server, you can't quite order an object and I'm using both people and projects in a selectbox, which needs to be ordered by name).
Am I doing it right? Are there better way? Or should I forget this completely and stick with the search loop as suggested in other question?
I'm trying to be as efficient as possible on both server and client side.
You basically doubled all the objects just to avoid loop. So, unless you have some bad performance issues, I would avoid that.
In case you really, really need a kind of hashmap, I would prefer storing the array's index instead of another copy of the object:
// array
var arr = [
{
"id": "8",
"name": "John Doe"
}
];
// lookup table
var lookup = {
"8": 0
}
Of course doing that, means you can't modifying the array's without rebuild the hashmap.
Generate it's quite simple:
var lookup = arr.reduce(function(lookup, item, index) {
lookup[item.id] = index;
return lookup;
}, {});
You can also use that to generate the object you mentioned your question:
var lookup = arr.reduce(function(lookup, item) {
lookup[item.id] = {name: item.name};
return lookup;
}, {});
But as I said it's something I would avoid.
Following code may help you. JSFIDDLE
var arr = [
{
"id": "8",
"name": "John Doe"
}
];
var obj = {};
for(var i=0; i< arr.length; i++){
obj[arr[i].id] = {name: arr[i].name};
}
console.log(obj);
var articles= {
"item1":{
"id":"155",
"name":"First Item",
"value":-5199.6
},
"item2":{
"id":"255",
"name":"Second Item",
"value":-424.91
}
}
var ids = [];
for(var item in articles) {
ids.push(articles[item]['id']);
}
console.log(ids);
This lib https://github.com/paularmstrong/normalizr makes it pretty easy to do. Both normalization and denormalization.
It can turn this
{
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [
{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}
]
}
into this
{
result: "123",
entities: {
"articles": {
"123": {
id: "123",
author: "1",
title: "My awesome blog post",
comments: [ "324" ]
}
},
"users": {
"1": { "id": "1", "name": "Paul" },
"2": { "id": "2", "name": "Nicole" }
},
"comments": {
"324": { id: "324", "commenter": "2" }
}
}
}
and the other way around.