I have an array of objects that can be updated by appending new elements to the array with the action function addNewCategory(category)
After the new category appended to the array, I want to rebuild a tree view that I created. The tree view uses the redux store object tree.
The tree gets build inside the action function buildCategoriesTree
Here are the functions:
export function addNewCategory(category) {
return {
type: ADD_NEW_CATEGORY,
payload: category
}
}
export function buildCategoriesTree(feecategories) {
let cats = feecategories.map(x => ({...x}));
let categories = {
name: 'Kategorien',
toggled: true,
children:
buildTreeFromCategories(cats)
};
return {
type: GET_CATEGORIES_TREE,
payload: categories
}
}
Now the problem is: when I call the buildCategoriesTree(...) function after the addNewCategory(...) function. The tree doesn't update. When I add another category, the tree updates with the category I added before. So it is alway one step behind.
How is it possible that the second function "waits" until the store is up to date? I read about react-thunk, but I don't know if this is the correct solution for this kind of problem?
Thanks a lot!
Related
I'm doing a library project (see the link for more explaination) with react, redux-toolkit and typescript.
I'm doing an addToWishlist funcionality: the user simply click on the star, the code dispatches the action which patch the database and the state is updated. There is another action to remove the book from the list.
In <Wishlist /> component I want of course render the titles and other data of each book. So I useAppSelector to retrieve wishlist and the entire list of books, and with help of filter and map I would get the corresponding books, but I'm not able to render them, because it gives me a ts error: index.d.ts(1373, 9): The expected type comes from property 'children' which is declared here on type 'DetailedHTMLProps<HTMLAttributes<HTMLUListElement>, HTMLUListElement>'
Important note about the structure
wishlist is an array of strings, which are the ids of the books.
The entire list of books instead, is an array of objects: every object is a book, with title, author, id etc.
My questions:
What is going wrong here?
Basically, is it better to save in the wishlist array the ids (like I've done) and so I've to retrieve the data of books in my <Wishlist /> component with filter and map, or is it better to save the entire book (or at least, the infos that I need), so that is more easy to have the data?
Which is the best practice, to render the list, in the first case, where I have to retrieve data using filter and map?
Here is the attempt:
const Wishlist = () => {
const dispatch = useAppDispatch();
const wishlist = useAppSelector(state => state.user.userInfo.wishlist);
const list = useAppSelector(state => state.catalogue.list);
const renderBooks = () => {
return wishlist.map(wishId => {
list.filter(book => {
if (book.id === wishId) {
return (
<li>{book.title}</li>
)
}
})
})
}
return (
<ul>
{renderBooks()}
</ul>
)
};
export default Wishlist;
Edited - maybe the problem is not the concatenation of methods, but something in the function calling?
Another more little example, with another solution, but same problem
interface fakeObj {
id: string;
name: string;
}
const fakeIds = ['5', '12', '878'] // this is my 'wishlist', containing only ids
const fakeList: fakeObj[] = [ // this is my 'list', containing objects, which were single books
{ id: '334', name: 'Tony' },
{ id: '5', name: 'Milly' },
{ id: '12', name: 'Jack' },
{ id: '7', name: 'Stew' },
{ id: '878', name: 'Mark' },
{ id: '87998', name: 'Greg' }
]
const renderArray = () => {
const arr: fakeObj[] = [];
fakeIds.forEach(id => {
fakeList.filter(obj => {
if (obj.id === id) {
arr.push(obj)
}
})
})
return arr; // the returned array is what I want: an array continaing the objects that i need
}
return (
<ul>
{/* The problem actually is here! */}
{renderArray()}
</ul>
)
You're not using .filter() or .map() correctly, which is what's causing the errors.
The comment above that you rejected was the correct solution to this problem.
The added edited example is even more confusing, because you're no longer trying to return renderable elements, as the return is an array of objects, which can't be rendering in a UL.
So, I'm going back to the initial example.
Problem #1:
You're trying to return a JSX element within the callback passed to .filter(). This is not how .filter() works. You're supposed to pass a callback to .filter() that will return TRUE or FALSE dependent on whether or not you want the element to be filtered in or filtered out. E.g. listOfCounts.filter( count => count > 5); would return a modified array of all elements in listOfCounts that are greater than 5.
Problem #2:
You're currently calling the .map() first, which is wrong, but then also calling the .filter() WITHIN the .map() instead of chaining the map AFTER the filter. Because right now, your .map() call is not even attempting to return a list of JSX elements representing the book titles you want to render. These calls are meant to be chained opposite of how you have it.
An example of how to use .map() that's very simple:
listOfNames.map(name => <div>{name}</div>);
A simple example combining .map() with .filter():
listOfNames.filter(name => name[0] == 'J').map(name => <div>{name}</div>);
which filters out all names that don't start with an uppercase J.
The solution above that Mr. Rafique provided you is correct / very close to correct depending on exact implementation, so I'd suggest you combine this added info with his solution.
This is more of a brainstorming question as I can't really seem to come up with a good solution.
I have a component which renders a tree based on some passed JSON (stored at the top level). Each node of the tree can have 0..n children and maps to a component defined by the JSON of that node (can be basically anything is the idea). The following is just an example and the names don't mean anything specific. Don't pay too much attention to the names and why a UserList might have children that could be anything.
JSON: {
data: {}
children: [
{
data: {}
children: []
},
{
data: {}
children: []
},
{
data: {}
children: [
{
data: {}
children: []
},
...etc
]
},
]
}
const findComponent = (props) => {
if (props.data.name === "userSelector") {
return <UserSelectorNode {...props}>;
} else if (props.data.name === "userInformation") {
return <UserInformationNode{...props}>; // example of what might be under a userSelectorNode
}
...etc
};
// render a user selector and children
const UserSelectorNode = (props) => {
const [selected, setSelected] = React.useState([])
// other methods which can update the JSON when selected changes...
return (
<div>
<UserSelector selected={selected}/> // does a getUser() server request internally
<div>
{props.data.children.map((child) => findComponent(child))}
<div>
</div>
);
};
This tree can be modified at any level (add/remove/edit). Adding/Editing is easy. The problem is remove operations.
Some children components use existing components which do things like getting a list of users and displaying them in a list (stored in state I have no access to). When a node on the tree is removed new components are made for every node that has to shift (JSON at the index is now different), which can be a lot. This causes a bunch of requests to occur again and sometimes state can be lost entirely (say the page number of a table to view users).
I'm pretty sure there is no way for the "new" UserSelector created when the JSON shifts to keep the same state, but I figured I may as well ask if anyone has dealt with anything similar and how they went about designing it.
The only way I can think of is to not actually reuse any components and re implement them with state stored somewhere else (which would suck), or rewrite everything to be able to take internal state as well as an external state storage if required.
EDIT: Added Sandbox
https://codesandbox.io/s/focused-thunder-neyxf. Threw it together pretty quick to only get a single layer of remove working which shows the problem.
I have an object in my redux state
const obj = {
name: 'name',
age: 2,
place: 0,
}
I show these values on the page but I want to make two of them editable so that the object can be updated.
For that I'm basically getting values from two inputs and sending them to my action
export const saveEditedData = data => dispatch => {
dispatch({
type: CHANGE_DATA,
data,
});
}
and then in reducer
case 'CHANGE_DATA':
return {
...state,
obj: {
...state.obj,
name: action.data.name,
age: action.data.age,
}
}
The problem that I'm facing is that if one value is updated and another is not then after this action my second value in empty.
My question is what is a good way to determine which field is changed and update only it?
So far I only came up with putting if else in action to dispatch certain thing. Maybe there is a better way?
You can 1. make each field update with individual actions, changeName, changeAge, etc, or 2. filter the action payload to get rid of the unwanted values before putting them in your obj:
case 'CHANGE_DATA':
return {
...state,
obj: {
...state.obj,
...action.data.filter(el => !!el)
}
}
(the !! notation is converting the array elements to booleans when filtering, not strictly necessary but sanitary)
EDIT: Sorry I misunderstood your data shape, see comments.
I am dealing with Proposals and locations.
A location can have multiple proposals.
The component is passed a location object (below as passedProps) to show all the proposals for this location :
<Proposals location={ location } key={location.id}/>
Here is are my redux props :
const mapStateToProps = (state , passedProps) => {
return {
proposals : state.propertyProposals[passedProps.location.id]
};
};
when adding a new proposal, I want to store their ids by location, so in a addition to storing the proposal, I am storing their Ids, in an array keyed by location Id.
The first proposal added and refreshed with new props just fine, however even though the second one is successfully pushed to the array, this is not triggering new props, so it does not "refresh" -- If I leave the route and come back I see the new 2nd proposal (which did not show the first time)
Here is the PROPOSAL_CREATE action for a new Proposal.
type :'PROPOSAL_CREATE',
payload :
{'e7ef104e-19ed-acc8-7db5-8f13839faae3' : {
id : 'e7ef104e-19ed-acc8-7db5-8f13839faae3',
locationId: '41e9c5d8-a520-7e3b-939a-12f784d49712'
}
}
here is the case which handles it :
case 'PROPOSAL_CREATE':
{
const proposal = Object.values(action.payload)[0];
const locationId = proposal.locationId;
let newState = {...state}
if (locationId in newState) {
newState[locationId].push(proposal.id)
} else {
newState[locationId] = [proposal.id]
}
return newState
}
Is there an obvious reason I am not seeing the change in the component for the second entry?
Thanks
There is one issue here. Your store state is not immutable. You have used below line to make a copy:
let newState = {...state}
Here it does make copy of object but it's shallow copy, hence your array object in newState and state have the same reference. That's why redux doesn't identify the change in store and hence props are not updated in sequence.
You can clone your state by below methods:
let newState = JSON.parse(JSON.stringify(state));
OR if you use jQuery then:
let newState = $.extend(true, {}, state);
I think this will surely fix your issue.
Based on your reducer logic i think that you did not specify action type.
The one of the redux conventions is the action recognition based on type property.
I bet that you forgot to specify that property.
var properAction = {
type: 'PROPOSAL_CREATE',
payload: {
{
'e7ef104e-19ed-acc8-7db5-8f13839faae3': {
id: 'e7ef104e-19ed-acc8-7db5-8f13839faae3',
locationId: '41e9c5d8-a520-7e3b-939a-12f784d49712'
}
}
}
I would recommend you to write action creators it will reduce your place for typos like that.
Cheers!
2 things:
I forgot that Arrays of an original object are still by reference. So even after
let newState = {...state}
newState[locationId]
has the same reference as
state[locationId]
As a result my original statement was mutating the original state, not creating a newState
and so
newState[locationId].push(proposal.id)
needed to be
newState[locationId] = state[locationId].concat(proposal.id);
or es6
newState[locationId] = [ ...state[locationId] , proposal.id ] ;
So I have a Stage in my application which contains different sets of data for each.
What I want to do is if I change my stage, that my child component refreshes with data from said stage.
I have this stage selection on the top of every page, in my App.jsx component.
This stage is handled by the state through Redux
....
changeStage: function (stageId) {
this.props.setStage(this.getStageById(stageId));
},
....
var mapDispatchToProps = function (dispatch) {
return {
setStage: function (data) { dispatch(stageService.setStage(data)); }
}
};
module.exports = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);
Now in my secondary (child - but deep down) component I have following:
shouldComponentUpdate: function (nextprops, nextstate) {
console.log("stage id in this component: " + this.state.stageId);
console.log("stage id in nextstate: " + nextstate.stageId);
console.log("stage id from the redux store: "+ this.props.stage.id)
return true;
},
....
var mapStateToProps = function (state) {
return {
stage: state.stage
};
}
but nextstate doesn't contain the updated stage but rather the previous stage, which makes sense.
The state.stageId also contains the previous - also still makes sense.
but the this.props.stage.id I expected to contain the newly selected stage but it doesn't.
It actually contains the previous stage so that means this gets called before the state is updated or is in process of updating the state.
So anyway my question, how can I get the new stage selected?
I do not want to pass the stage object down to all my children because that would make it not readable at all anymore and would be sloppy in my opinion.
Anyone have any idea?
The
You simply need to use nextprops.stage.id instead of this.props.stage.id to get the nextprops passed to your component.