ReactJS state vs props - where to keep source of truth for a To Do app - reactjs

I am starting on ReactJS from Angular world and really confused with having to keep track of json data object and how to edit and remove items.
var myTasks = {
"tasks": [
{
"name": "HOME tasks",
"type": "HOME",
"tasklist": [
{
"id": 1,
"todo_name": "go home",
"user": "scotty",
"actions": [
{
"name": "delete",
"action": "delete.php"
}
]
},
{
"id": 2,
"todo_name": "go to work",
"user": "scotty",
"actions": [
{
"name": "delete",
"action": "delete.php"
}
]
}
]
},
{
"name": "WORK tasks",
"type": "WORK",
"tasklist": [
{
"id": 1,
"todo_name": "go home",
"user": "scotty",
"actions": [
{
"name": "delete",
"action": "delete.php"
}
]
},
{
"id": 2,
"todo_name": "go to work",
"user": "scotty",
"actions": [
{
"name": "delete",
"action": "delete.php"
}
]
}
]
}
]
}
Let's say I break it up into components (code is incorrect and I am just trying to illustrate the idea).
var Tasks = React.createClass({
<TaskListPerType/>
});
var TaskListPerType = React.createClass({
<TaskListPerType_Actions />
});
var TaskListPerType_Actions = React.createClass({
});
I was thinking of creating a state of data on Tasks component and then pass the states as props to the child components. I want to add a delete function on the TaskListPerType_Actions component.
Question is does each component has its own states and when I add functions such as delete, do I only need to act on the state within that component and when I act on that state, does it automatically update the state in the parent where it's passed to the children as props?

For React to make sense, you have to first forget about MVC javascript. Sure, you can use MVC, but React is made for one-direction data binding. By that, I mean that the data itself (or a class containing the data, which emits events) is what triggers the changes in the view. Check out Flux and this concept will become more clear.
Typically, you don't want state to be on every component. Each component should be concerned with only it's own state. If, in the event there is storage required, that should be separated into it's own object and that object should be the source of the change.
There is no data mutation happening in React, each state change triggers a complete re-render of the view. This is confusing at first, but it becomes easier as you start to understand the way React wants your data to flow.
Here is a full example of a React application implemented using Flux which, IMO, is the most natural way to create React apps.

Related

Update single record in nested state object, react-redux

I am working on a grid structure where user can add sections, sub-sections or items dynamically. I am managing that things in my redux state object. UI of my grid is as following :
I want to update a single row record instead of reloading whole grid again. For that, whenever user changes any cell value of row i am calling update-row api and on success of that i am trying to update that value in reducer using following code.
case UPDATE_ORDER_LINES_SUCCESS:
let stateData = state.get(`GridData`);
const dataIndex = stateData.children.findIndex(
(listing) => listing.id === action.row.id // row id which is updated
);
stateData[0].children[dataIndex] = action.row;
let data = Object.assign(stateData, { children: stateData.children });
state = state.set(`GridData`, [data]);
This code is working fine for first level of children records (as per json object) but problem occur if user update value of nth level children record. How can i update that row record in my redux state ?
My current redux state sample is :
{
"views": [
{
"id": "5e6b8961ba08180001a10bb6",
"viewName": "house",
"description": "house view",
"name": "house",
"children": [
{
"id": "5e6b8961ba08180001a10bb7",
"viewName": "house",
"sectionName": "Temporary",
"sectionId": "SEC-02986",
"description": "Temporary",
"sequenceNumber": 4,
"refrenceId": "SEC-02986",
"children": [
{
"id": "5e590df71bbc71000118c109",
"lineDescription": "AutoPickPack01",
"lineAction": "Rent",
"quantity": 5,
"deliveryDate": "2020-02-29T06:00:00+11:00",
"pickDate": "2020-02-28T06:00:00+11:00",
"pickupDate": "2020-03-01T06:00:00+11:00",
"prepDate": "2020-02-28T06:00:00+11:00",
"returnDate": "2020-03-01T06:00:00+11:00",
"shippingDate": "2020-02-29T06:00:00+11:00",
"unitPrice": 7000,
"children": [
{
"id": "5e590df71bbc71000118c10a",
"orderId": "Ord-05788_1",
"lineNumber": "01a7b77c-792a-4edb-9b73-132440621968",
"purchaseOrderNumber": null,
"lineDescription": "29Janserial",
"lineAction": "Rent",
"quantity": 5,
"pricingMethod": "Fixed",
"displayUnit": "Days",
"unitPrice": 0,
"chargeAmount": 0,
"pickDate": "2020-02-17T06:00:00+11:00",
"prepDate": "2020-02-28T06:00:00+11:00",
"shippingDate": "2020-02-29T06:00:00+11:00",
"deliveryDate": "2020-02-29T06:00:00+11:00",
"pickupDate": "2020-03-01T06:00:00+11:00",
"returnDate": "2020-03-01T06:00:00+11:00",
"name": "29Janserial",
"description": "29Janserial",
"discountAmount": "",
"discountPrice": ""
}
]
}
]
}
]
}
]
}
What is the best way to update nested children row data in reducer ?
As redux doesn't allow to mutate the current state and return it back, it's hard to modify a nested child. Although its highly discouraged to
have this kind of nested structure in redux, rather it should be normalized as #bsapaka answered. But if you still want to update the nested
object and return the whole state as an immutable one, immer should be your friend. immerJS has been so popular for handling immutable states.Although
Install immer and redux-immer in your case
yarn add immer redux-immer
In your reducers.js file where all reducers have been combined using combineReducers
import produce from 'immer';
import { combineReducers } from 'redux-immer';
// Replace your current combineReducers with
combineReducers(produce, { /* Object of all reducers */ });
In your current reducer file
import product from 'immer';
const findNestedChild = (arr, itemId) => (
arr.reduce((a, item) => {
if (a) return a;
if (item.id === itemId) return item;
if (item['children']) return findItemNested(item['children'], itemId)
}, null)
);
case UPDATE_ORDER_LINES_SUCCESS:
return produce(state, draftState => {
const { row: newChild, row: { id }} = action;
let child = findNestedChild(draftState.views, id);
child = newChild;
});
You should normalize your state, which flattens the tree, and entities become associated by id references instead of direct nesting.
For example
{
"entities": {
"orders": {
"o1": { "id": "o1", "productIds": ["p1", "p2"] },
"o2": { "id": "o2", "productIds": ["p2", "p3"] },
"o3": { "id": "o2", "productIds": ["p3"] }
},
"products": {
"p1": { "id": "p1", "orderIds": ["o1"] },
"p2": { "id": "p1", "orderIds": ["o1", "o2"] },
"p3": { "id": "p1", "orderIds": ["o2", "o3"] }
},
"views": {
"v1": { "id": "v1", "childIds": ["v1.1", "v1.2"] },
"v1.1": { "id": "v1.1", "parentId": "v1" },
"v1.2": { "id": "v1.2", "parentId": "v1" }}
},
"ids": {
"orders": ["o1", "o2", "o3"],
"products": ["p1", "p2", "p3"],
"views": ["v1", "v1.1", "v1.2"]
}
}
There's more upfront work of finding the correct model and transforming the raw data into it, but you save a lot of time not having to deal with updates that are nested or affect multiple areas of data.
Redux docs on normalizing
A (de)normalization transformation tool
A reducer utility library to manage normalized state

Why is refetchQueries needed?

I am following a tutorial on GraphQL, in the video the author does not use refetchQueries for a deleteMutation and all works well with UI updates and mutation. But here in the project sandbox code is updated and refetchQuery is now used for this operatio on Job component -> line 20 -> deleteJob(): codeSandBox.
I have this similar problem in my app that does not update the UI automatically without refetchQueries done everywhere. Shouldn't Apollo be applying automatically the cache of Apollo via apollo-cache-inmemory, perform mutation and update UI in this kind of mutation if I understand it right.
Example out of the box with apollo-boost:
export default gql`
mutation deleteItem($id: uuid!) {
delete_item(where: {id:{_eq: $id }}){
returning {
id
}
}
}`;
const onDeleteItem = (id) => {
deleteItem({
variables: { id },
});
};
Any suggestions or experiences on this?
The answer is relatively simple: There is no universal way in GraphQL to tell a client that an entity was deleted. Let's first compare this to an update mutations. Imagine we are updating one of the jobs that we already have in our cache. First the cache (simplified, not actually quite how it looks inside of Apollo):
{
"Query": {
"jobs": ["Job:1", "Job:2"],
},
"Job:1": {
"__typename": "Job",
"id": 1,
"company": "Big Corp",
"title": "Sales Specialist"
},
"Job:2": {
"__typename": "Job",
"id": 2,
"company": "Big Corp",
"title": "GraphQL Expert"
}
}
If Apollo now gets an answer from an update mutation that looks like the following:
{
"data": {
"updateJob": {
"__typename": "Job",
"id": 2,
"company": "Big Corp",
"title": "GraphQL Unicorn"
}
}
}
It can use the dataIdFromObject function to understand that the object belongs to the cache key "Job:2" in our normalised cache. Apollo can assume that this version is newer than the old one and merge the keys with preference of the newer result. Our cache now looks like this:
{
"Query": {
"jobs": ["Job:1", "Job:2"],
},
"Job:1": { ... },
"Job:2": {
"__typename": "Job",
"id": 2,
"company": "Big Corp",
"title": "GraphQL Unicorn" // updated!
}
}
Then the "jobs" query will automatically update with the new job because it is just referencing the job and is not storing the entity itself. Great! But now compare the result from the delete function:
{
"data": {
"deleteJob": {
"returning": {
"id": 2,
}
}
}
}
The result of this query could be anything. Apollo cannot know that you have just deleted a job with a certain id. Maybe if GraphQL had something in the specification like a magical "__isDeleted" and we would get something like:
{
"data": {
"deleteJob": {
"__typename": "Job",
"__isDeleted": true,
"id": 2,
}
}
}
}
We could give our cache implementation the hint that entities with __isDeleted: true should be removed from all referencing queries. But unfortunately this does not exists. This is not to bad though, we can either use refetchQuery to trigger a refetch of the other query or we can manually update the other query:
const deleteJob = useMutation(DELETE_JOB, {
update(store, response) {
const data = store.readQuery({ query: GET_JOBS });
data.jobs = data.jobs.filter(job => job.id !== response.deleteJob.returning.id);
store.writeQuery({ query: GET_JOBS, data });
}
});

Rendering a list of optional components in React

In my React app, I have some optional components that need to be rendered based on user's preferences. I get a list of these components and their order in an array from a call to my API.
The array will look like this:
[
{ "id": 23, "name": "WhetherComponent", "label": "Local Whether", "displayOrder": 1 },
{ "id": 477, "name": "NFLComponent", "label": "NFL Results", "displayOrder": 2 },
{ "id": 59, "name": "NBAComponent", "label": "NBA Results", "displayOrder": 3 }
]
And in the parent component, I need to render them within <div> tags. Clearly, I need to import these components in the parent component but is there anything special I need to do to render them?
Use lazy() to achieve this:
render() {
return this./*state? props?*/.yourArray.map(ele => {
const Comp = React.lazy(() => import('./' + ele.name));
return <div><Comp /></div>; // You said you need to render them within <div>s
});
}
Of course, you might want to sort() the array based on displayOrder beforehand.

Best way to store presentation-specific data in react-redux application

I know the best practice is not using a redux store to save any kind of visualization-related data. But I don't see any way to avoid that in my project. I have a simple store like this:
{
pages: {
byId: {
'0': {
id: '0',
canvases: {
byId: {
'0': {
id: '0',
layers:
{
byId: ['0':{uid:'0'}],
allIds: ['0']
}
}
},
allIds: ['0']
},
selectedId: '0'
}
},
allIds: ['0']
}
}
It just stores the document filled by pages. Each page may have one or more canvases. Each canvas has zero or more layers. Visually each page/canvas/layer is a tree of nested blocks. My idea is putting a selection frame with some handles on top of z-index of my HTML when user clicks onto a layer. The problem is that selection component is in a different DOM tree relatively to the page but at the same time I need to know a bounding rectangle of my page, canvas and layer to overlay the selection correctly. What is the best way to do that? Do I need to use a redux store to save my bounding area?
I think a good solution here will be normalizr. It takes a json like this:
{
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [
{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}
]
}
and turns it into something like 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" }
}
}
}
Take a look at the docs for normalizr, I think it can help you.
I decided to choose MobX + MST framework: https://github.com/mobxjs. This one is covering snapshots to easily manage application states. I am even able to connect Redux Dev Tools to track my state at runtime. In addition to that it has a support of volatile states which makes sense for storing some temporary data like drag and drop mouse offset or some not tied to store visual data. And another one thing is that undo/redo logic can be implemented easily. I am originally from OOP world so MobX/MST is closer to my mindset than Flux/Redux concepts.

Angular schema form destroyStrategy works only on siblings that are array of objects. It does not work on other siblings

I have not been successful in getting the destroy strategy to work on any sibling properties or sibling objects. It only works on sibling array of objects. Please check this example:
$scope.schema = {
"type": "object",
"properties": {
"propertyOne": {
"type": "string",
"enum": ["option1", "option2"],
"title": "Property One Select"
},
"propertyTwo": {
"type": "string",
"enum": ["option3", "option4"],
"title": "Property Two Select"
},
"objectOne": {
"type": "object",
"properties": {
"objectOnePropertyThree": {
"type": "string",
"enum": ["option5","option6"],
"title": "Property Three Select"
}
}
},
"arrayOfObjects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"arrayObjectPropertyFour": {
"type": "string",
"enum": ["option7","option8"],
"title": "Property Four Select"
}
}
}
}
},
"required": ["propertyOne"]
};
$scope.form = [{
"key": "propertyOne"
}, {
"key": "propertyTwo",
"condition": "model.propertyOne === \"option1\""
},{
"key": "objectOne.objectOnePropertyThree",
"condition": "model.propertyOne === \"option1\""
},{
"key": "arrayOfObjects",
"condition": "model.propertyOne === \"option1\""
},
{
"type": "submit",
"title": "Save"
}];
http://jsfiddle.net/mutharasus/dp18a70b/
In here if you select the first dropdown to "Option1" then select all the other dropdowns and save. Then go back and switch the first dropdown to "Option2" and save you can see that only the very last array of objects is removed with the destroy strategy.
Am I doing something wrong or is this a bug in angular-schema-form? I looked under the issues that are currently open in the github project and I do not see an open issue about this.
You're correct, it's not currently behaving the way that you expect when an individual field is removed from the view because of a condition.
Here's what's happening: under the "old" bundled decorators for ASF, each field-type decorator is rendered on the page within an outer tag. The contents of the appropriate field template are then processed and rendered. Condition logic applies to everything within the tag, but not to the tag itself. Normally, this would be fine, but the destroyStrategy logic was assigned to the $destroy event of the tag. The end result is that the $destroy event will never fire unless the entire tag would be removed from the DOM. This is why the model values in the array of objects are being cleaned up - the container is removed when the "model.propertyOne === 'option1'" condition fails, which cascades the $destroy event to each object in the array.
I think that this got overlooked with the creation and release of the new builder, because I raised the issue at the end of the PR for the feature (https://github.com/Textalk/angular-schema-form/pull/371).
On the bright side, the new builder approach (which you can use by adding the bootstrap-decorator file from https://github.com/Textalk/angular-schema-form-bootstrap) doesn't have this issue. Instead, the destroyStrategy logic is applied via directive to the form fields because the tag is no longer used. Unless you have a need to stay with the old decorators at this time, I suggest grabbing the new ones and giving them a try.
Let us know how it goes!

Resources