Redux saga value cannot apply or use immediately - javascript

Redux store cannot use immediately.
Backgound:
I have created a parent component and a child component.
The parent component will fetch a list of data, save to redux store,and generate child component.
example:
result:[
{
key: 1,
roomNo: '01',
status: 'OCCUPIED',
},
{
key: 2,
roomNo: '02',
status: 'UNOCCUPIED',
},
]
Then, there will be 2 child component (which is a button). And if I click the child button. The application will call DB again for more detail and save to redux store also.
detailResult: [
{
key: 1,
roomNo: '01',
status: 'OCCUPIED',
roomColor: 'red',
},
]
And since I would like to check whether the status is the same (result.roomNo="01"(OCCUPIED) and detailResult.roomNo="01"(OCCUPIED) ) (to ensure the data is the most updated one when user click the button)
Therefore, I write the following function in child component:
const { reduxStore } = useSelector((state) => ({
reduxStore : state.reduxStore ,
}));
const retrieve = useCallback(
(params) => {
dispatch(Actions.wardFetchRoom(params));
},
[dispatch]
);
const handleClick = (event) => {
const params = { roomNo: roomNo};
retrieve(params);
console.log('Step1', props.bedNo);
console.log('Step2', props.status);
console.log('Step3', reduxStore.room.status);
//function to match props.status === reduxStore.room.status
};
The problem comes when I click the the child button. The data always compare with previous store data but not current store data
T0: render finish
T1: click <child room="1">
T2: console: "Step1 = 1, Step2 = OCCUPIED, Step3 = NULL" (I expect "Step3 = OCCUPIED",since retrieve(params) is completed and the store should be updated at this moment)
T3: click <child room="2">
T4: console: "Step1 = 2, Step2 = UNOCCUPIED, Step3 = OCCUPIED" (This "Step3" data is come from T2, the data is delay for 1 user action)
Therefore, how can I change my code to use the store data immediately?

Related

Creating a createSelector in Redux that does not rerender unrelated components

I have a simple document maker that allows users the create multiple fields of two types (input field and label field). They can also rearrange the field via drag and drop. My data structure is based on the Redux guide on data normalization.
LIST field is input type (e.g. children id 21 and 22) - onChange will dispatch action and allows the input text to modified without re-rendering other fields.
With LABEL field we allow users to select from a dropdown list of only 3 labels (Danger, Slippery, Flammable). Selecting one with remove it from the dropdown list to prevent duplicate label. For example, if there are "Danger" and "Slippery" in the field, the dropdown field will only show one option "Flammable". To achieve this, I use createSelector to get all children and filter them base on their parent (fieldId). This gives me an array of existing labels ["Danger","Slippery"] in that fieldId. This array will then be used to filter from a fixed dropdown array of three options with useEffect.
Now whenever I update the input text, it will also re-render the LABEL field (based on the Chrome React profiler).
It does not affect performance but I feel like I am missing something with my createSelector.
Example:
export const documentSlice = createSlice({
name: "document",
initialState: {
fields: {
1: { id: 1, children: [11, 12] },
2: { id: 2, children: [21, 22] },
},
children: {
11: { id: 11, type: "LABEL", text: "Danger", fieldId: 1 },
12: { id: 11, type: "LABEL", text: "Slippery", fieldId: 1 },
21: { id: 21, type: "LIST", text: "", fieldId: 2 },
22: { id: 22, type: "LIST", text: "", fieldId: 2 },
},
fieldOrder:[1,2]
},
});
createSelector
export const selectAllChildren = (state) => state.document.children;
export const selectFieldId = (state, fieldId) => fieldId;
export const getChildrenByFieldId = createSelector(
[selectAllChildren, selectFieldId],
(children, fieldId) => {
const filterObject = (obj, filter, filterValue) =>
Object.keys(obj).reduce(
(acc, val) =>
obj[val][filter] !== filterValue ? acc : [...acc, obj[val].text],
[]
);
const existingChildren = filterObject(children, "fieldId", filterId);
return existingChildren;
}
);
After more reading up, this is what finally works for me. Hopefully someone will find it useful.
Given the normalized data, the object reduce function can be simplified.
slice.js
export const getChildrenByFieldId = createSelector(
[selectAllChildren, selectFieldId],
(children, fieldId) => {
// get the children from fields entry => array [11,12]
const childrenIds = state.document.fields[fieldId].children
let existingChildrenText = [];
// loop through the created array [11,12]
childrenIds.forEach((childId) => {
// pull data from the individual children entry
const childText = children[childId].text;
existingChildrenText.push(childText);
});
return existingChildrenText;
}
);
To prevent re-render, we can use shallowEqual comparison on the output array to compare only the values of the array.
//app.js
import { shallowEqual, useSelector } from "react-redux";
const childrenTextData = useSelector((state) => {
return getChildrenByFieldId(state, blockId);
}, shallowEqual);

React-select is going blank when the options array changes (codesandbox included)

I am trying to use react-select in combination with match-sorter as described in this stackoverflow answer (their working version). I have an initial array of objects that get mapped to an array of objects with the value and label properties required by react-select, which is stored in state. That array is passed directly to react-select, and when you first click the search box everything looks good, all the options are there. The onInputChange prop is given a call to matchSorter, which in turn is given the array, the new input value, and the key the objects should be sorted on. In my project, and reproduced in the sandbox, as soon as you type anything into the input field, all the options disappear and are replaced by the no options message. If you click out of the box and back into it, the sorted options show up the way they should. See my sandbox for the issue, and here's the sandbox code:
import "./styles.css";
import { matchSorter } from "match-sorter";
import { useState } from "react";
import Select from "react-select";
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: <div>{obj.name}</div>,
name: obj.name
};
};
export default function App() {
const [options, setOptions] = useState(objs.map((obj) => myMapper(obj)));
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(matchSorter(options, val, { keys: ["name", "value"] }));
}}
/>
);
}
I am sure that the array in state is not getting removed or anything, I've console logged each step of the way and the array is definitely getting properly sorted by match-sorter. It's just that as soon as you type anything, react-select stops rendering any options until you click out and back in again. Does it have something to do with using JSX as the label value? I'm doing that in my project in order to display an image along with the options.
I had to do two things to make your code work:
Replaced label: <div>{obj.name}</div> with label: obj.name in your mapper function.
I am not sure if react-select allows html nodes as labels. Their documentation just defines it as type OptionType = { [string]: any } which is way too generic for anything.
The list supplied to matchSorter for matching must be the full list (with all options). You were supplying the filtered list of previous match (from component's state).
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: obj.name, // -------------------- (1)
name: obj.name
};
};
const allOptions = objs.map((obj) => myMapper(obj));
export default function App() {
const [options, setOptions] = useState(allOptions);
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(
matchSorter(
allOptions, // ----------------> (2)
val,
{ keys: ["name", "value"]
}
));
}}
/>
);
}

React Show data with page refresh on Button click

I have array of object as data. The data is displayed on initial page load. When Clear Book button is called with clearBooks() function, I set array to empty (No Data is displayed) and change button value to Show Books
What I am trying to achieve is when Show Books button is clicked I would like to show all objects as before. I though of refreshing page with window.location.reload(); when Show Books is clicked (If there are better solution, open to use them). I need help to achieve this functionality in code
main.js
const clearBooks = () => {
if (buttonTitle === "Clear Books") {
setButtonTitle("Show Books");
}
setBooksData([]);
};
return (
<section className="booklist">
{booksData.map((book, index) => {
return (
<Book key={index} {...book}>
</Book>
);
})}
<div>
<button type="button" onClick={clearBooks}>
{buttonTitle}
</button>
</div>
</section>
);
Data
export const books = [
{
id: 1,
img: "https://m.media/_QL65_.jpg",
author: " A ",
title: "B",
},
{
id: 2,
img: "https://m.media/_QL65_.jpg",
author: " A ",
title: "B",
},
You can achieve this by using useState hook.
const books = [
{
id: 1,
img: "https://m.media/_QL65_.jpg",
author: "A",
title: "B",
},
{
id: 2,
img: "https://m.media/_QL65_.jpg",
author: " A ",
title: "B",
},
]
const [bookList, setBookList] = useState(books);
const clearBooks = () => {
setBookList([]);
}
const showBooks = () => {
setBookList(books);
}
Set bookList to empty when you need to clear book list and to books object when yoou want to show your list books.
You could store the originally fetched data into a separate state and reset the booksData array by setting its value equal to that state on the Show Books click:
const [originalBooksData, setOriginalBooksData] = useState([]);
const [booksData, setBooksData] = useState([]);
const fetchBooks = async () => {
...
// After booksData have been fetched set originalBooksData equal to it
setOriginalBooksData(booksData)
}
const clearBooks = () => {
if (buttonTitle === 'Clear Books') {
// Clear the books
setButtonTitle('Show Books');
setBooksData([]);
} else {
// Reset the books
setBooksData(originalBooksData);
}
};
...
Of course, you will need to set the originalBooksData equal to booksData when the data are loaded.
This will prevent the need to refresh the page when clearing the data.

How to change/add value of a certain row of an array using useState Hook in React

I have a following array,
const bio = [
{id: 1, name: "Talha", age: 26},
{id: 2, name: "Ayub", age: 22}
]
Here is a complete code of mine,
import './App.css';
import React, {useState} from 'react';
const bio = [
{id: 1, name: "Talha", age: 26},
{id: 2, name: "Ayub", age: 22}
]
const App = () => {
const [bioData, setbioData] = useState(bio);
const clear_function = () => setbioData([])
return (
<div className="App">
{
bioData.map((arr) =>
<>
<h3 key={arr.id}>Name: {arr.name}, Age: {arr.age}</h3>
<button onClick={() => clear_function()}>Click me</button>
</>
)}
</div>
);
}
export default App;
Now, this code hooks the whole array to useState. Whenever I click the button "Click me", it sets the state to empty array and returns empty array as a result.
But what if I want to remove a specific row of the array? I have made two buttons, each for one row. What I want to do is if I click the first button, it removes the first row from the array as well and keeps the 2nd array.
Similarly, if I want to change a specific attribute value of a specific row, how can I do that using useState hook? For example, I want to change the name attribute of the 2nd row. How is it possible to do this with useState?
And same case for addition. IF I make a form and try to add a new row into my bio array, how would I do that? I searched it up on Google, found a similar post as well but the answer given in it wasn't satisfactory and did not work, which is why I am asking this question again.
If I understood the question right, I think you can pass the updated object to set state and that'll be it.
To change a particular object in array, do something lie this:
// Update bio data with id = 2
setbioData(prevValue =>
[...prevValue].map(el =>
el.id === 2 ? ({...el, name:'new name'}) : el)
)
Also, you have set key at the wrong place
Full refactored code:
const bio = [
{id: 1, name: "Talha", age: 26},
{id: 2, name: "Ayub", age: 22}
]
const App = () => {
const [bioData, setbioData] = useState(bio);
const clear_function = (id) => {
setbioData(prevValue =>
[...prevValue].map(el =>
el.id === id ? ({...el, name:'new name'}) : el)
)
}
return (
<div className="App">
{
bioData.map((arr) =>
<div key={arr.id}>
<h3>Name: {arr.name}, Age: {arr.age}</h3>
<button onClick={() => clear_function(arr.id)}>Click me</button>
</div>
)}
</div>
);
}
Adding a row to the array with react useState would be as simple as the following:
function addRow(objToAdd){
//get old data and add new row
let newBioData = bioData.push(objToAdd)
setBioData(newBioData)
}
To remove a specific row you would need to find the object within the bioData array, remove it and set the new bioData like so:
function removeRow(rowId){
//Filter the array by returning every object with a different Id than the one you want to remove
let newBioData = bioData.filter(function (value, index, arr){ return value.id != rowId })
setBioData(newBioData)
}
To update the state you could use the spread operator like so:
function updateRow(rowId, data){
//Iterate list and update certain row
bioData.filter(function (value, index, arr){
//Row to update
if(value.id == rowId){
return {name: data.name, age: data.age}
}else{
//Nothing to update, return current item (spread all values)
return {...value}
}
})
setBioData ([])
}
I'm not sure if I fully understand your question, but when you want to change the state in a functional component, you can't directly change the state object, instead you need to create a new desired state object, and then set the state to that object.
for example, if you want to add a new row to your array, you'll do the following:
setbioData((currBioData) => [...oldBioData, {id: 3, name: "Bla", age: 23}]);
When you want to change the state based on the current state, there's an option to send the setState function a callback function which accepts the current state, and then using the spread operator we add a new row to the array.
I'm not going to get into why it is better to pass setState a function to modify current state but you can read about it in React official docs
Looks like there are 3 questions here. I'll do my best to help.
"what if I want to remove a specific row of the array?"
One way is to filter your current array and update the state with the new array.
In your example: onClick={()=>setbioData(arr.filter(row=>row.id !== arr.id))}
"if I want to change a specific attribute value of a specific row, how can I do that using useState hook?"
You will need to create a new array with the correct information and save that array to state.
In your example: [... bioData, {id:/row_id/,name:/new name/,age:/new age/}]
"IF I make a form and try to add a new row into my bio array, how would I do that"
In this case, you would push a new object into your array. In your example:
setbioData(previousData=>previousData.push({id:previousData.length+1,name:/new name/,age:/new age/})
I hope this helps you, best of luck
import './App.css';
import React, {useState} from 'react';
const bio = [
{id: 1, name: "Talha", age: 26},
{id: 2, name: "Ayub", age: 22}
]
const App = () => {
const [bioData, setbioData] = useState(bio);
const clear_function = (i) => {
let temp = bioData.slice()
temp = temp.splice(i, 1)
setbioData(temp)
}
return (
<div className="App">
{
bioData.map((arr, i) =>
<>
<h3 key={arr.id}>Name: {arr.name}, Age: {arr.age}</h3>
<button onClick={() => clear_function(i)}>remove row</button>
</>
)}
</div>
);
}
export default App;
Same way you need to just to update a specific attribute just update that element in the temp array and do setBioData(temp)
Similarly, If you want to add another row do temp.push({id: 1, name: "Talha", age: 26}) and do setBioData(temp)

How can I get the current value of an input which is part of an array of objects?

So I am trying to fill an array with data. I am facing one problem.
1 - I am trying to find an index for every proper key in the array of objects. But I get an error once a new input is added. Yes, I am adding inputs dynamically too.
The inputs gets added well.
This is for example, how the data should look before been send to the backend. Like this is how the final object should be shaped:
{
"previous_investments" : [
{"name" : "A Name", "amount" : 10000},
{"name" : "Some Name", "amount" : 35000}
]
}
Seems to be easy but I am having a hard time.
This is how my main component looks:
const PreviousInvestments = ({
startupFourthStepForm,
previousInvestmentsInputs,
startupFourthStepFormActionHandler,
previousInvestmentsInputsActionHandler,
}) => {
const handleMoreInputs = async () => {
await startupFourthStepFormActionHandler(
startupFourthStepForm.previous_investments.push({
name: '',
amount: undefined,
}),
);
await previousInvestmentsInputsActionHandler(
`previous_investments-${previousInvestmentsInputs.length}`,
);
};
return (
<div className="previous-investments-inputs">
<p>Previous Investments</p>
{previousInvestmentsInputs.map((input, index) => (
<div key={input}>
<FormField
controlId={`name-${index}`}
onChange={e => {
startupFourthStepFormActionHandler({
// HERE IS WHERE I THINK I AM PROBABLY FAILING
previous_investments: [{ name: e.currentTarget.value }],
});
}}
value={startupFourthStepForm.previous_investments[index].name}
/>
</div>
))}
<Button onClick={() => handleMoreInputs()}>
+ Add more
</Button>
</div>
);
};
export default compose(
connect(
store => ({
startupFourthStepForm:
store.startupApplicationReducer.startupFourthStepForm,
previousInvestmentsInputs:
store.startupApplicationReducer.previousInvestmentsInputs,
}),
dispatch => ({
previousInvestmentsInputsActionHandler: name => {
dispatch(previousInvestmentsInputsAction(name));
},
startupFourthStepFormActionHandler: value => {
dispatch(startupFourthStepFormAction(value));
},
}),
),
)(PreviousInvestments);
In the code above, this button adds a new input and also it adds a new object to the array using the function handleMoreInputs:
<Button onClick={() => handleMoreInputs()}>
+ Add more
</Button>
This is the reducer:
const initialState = {
startupFourthStepForm: {
previous_investments: [{ name: '', amount: undefined }],
},
previousInvestmentsInputs: ['previous_investments-0'],
}
const handlers = {
[ActionTypes.STARTUP_FOURTH_STEP_FORM](state, action) {
return {
...state,
startupFourthStepForm: {
...state.startupFourthStepForm,
...action.payload.startupFourthStepForm,
},
};
},
[ActionTypes.PREVIOUS_INVESTMENTS_INPUTS](state, action) {
return {
...state,
previousInvestmentsInputs: [
...state.previousInvestmentsInputs,
action.payload.previousInvestmentsInputs,
],
};
},
}
The funny thing is that I am able to type in the first input and everything goes well. But once I add a new input, a second one, I get this error:
TypeError: Cannot read property 'name' of undefined
43 | controlId={startupFourthStepForm.previous_investments[index].name}
Wo what do you think I am missing?
The handler for ActionTypes.STARTUP_FOURTH_STEP_FORM is defined to be invoked with an object for startupFourthStepForm in the payload and effectively replace it.
Where this handler is invoked, you need to ensure that it is called with previous_investments field merged with the new value
startupFourthStepFormActionHandler({
...startupFourthStepForm,
previous_investments: [
...startupFourthStepForm.previous_investments.slice(0, index),
{
...startupFourthStepForm.previous_investments[index],
name: e.currentTarget.value
},
...startupFourthStepForm.previous_investments.slice(index+1,)
],
});
I suggest to refactor away this piece of state update from the handler to the reducer so that updates to the store are reflected in co-located.
This can be done by passing the index of the item in previous_investments as a part of the payload for ActionTypes.STARTUP_FOURTH_STEP_FORM action.

Categories