I have a component where through map I show all menu parts. How can I limit access(make it hidden) if user right for that part of menu is equal to 0?
const Aside: React.FunctionComponent = () => {
const[hasRight, setHasRight] = useState(false);
useEffect( () => {
getUserContext()
.then((response) => {
Object.keys(response.data.acl.rights).forEach(role => {
const right = response.data.acl.rights[role];
Object.keys(right).forEach(val => {
right[val] > 0 ?
setHasRight(true) :
setHasRight(false);
});
});
return response.data.acl;
})
}, [hasRight]);
return (
<div className="Aside">
{hasRight?
pages.map((page, index) => {
return (
<Link className="Aside__link" key={index} to={page.link}>
<img className="Aside__link-image" src={page.img} alt="page__img" />
<p>{page.title}</p>
</Link>
);
}): null}
</div>
);
};
In this part I hide all menu if it does not have access. I need to check each menu item to see if there is access to it.
Each user has many different rights for many different pages, so instead of creating hasRight as a boolean state, you need to store a keyed object showing whether they have rights for each page. I'm assuming that if the user has multiple roles, they would have rights if any of their roles have rights.
type UserRights = Record<string, boolean>;
const [userRights, setUserRights] = useState<UserRights>({});
On the rendering side of things, we need to check whether they have the right for each page separately. Instead of a ternary with null, we can use array.filter() to filter the pages before mapping them to JSX.
const canView = (page: PageConfig): boolean => {
return userRights[page.acl];
}
<div className="Aside">
{pages.filter(canView).map((page, index) => {
Prior to the response being loaded, there should be no viewable pages.
Inside our useEffect, we need to change the setState callbacks to support our keyed object format. Instead of using Object.keys to iterate, we can use Object.values and Object.entries (provided that your tsconfig has lib: es2017 or newer). I think this is correct, but you may need to tweak based on your response structure.
useEffect(() => {
getUserContext()
.then((response: Response) => {
Object.values(response.data.acl.rights).forEach(roleRights => {
Object.entries(roleRights).forEach(([name, number]) => {
setUserRights(existing => ({
...existing,
[name]: existing[name] || number > 0 // true if true true for any role
}))
});
});
})
}, []);
The dependency for the useEffect should be the user id or something like that, otherwise use an empty array [] to run it once.
Typescript Playground Link
Related
How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
I have an array of objects each with name, height and mass. I have a component that gets the names and displays them to the browser. What I'm trying to do is, in a separate component, get the height and mass that correlates to each name.
For example I have:
[
{name: 'Luke Skywalker', height: '172', mass: '77'},
{name: 'C-3PO', height: '167', mass: '75'}
]
I should mention I'm using react for this project. My Component is below:
export default function Character(props) {
const [charStats, setCharStats] = useState("");
const [currentName, setCurrentName] = useState("");
const { name } = props;
useEffect(() => {
axios.get(`${BASE_URL}`)
.then(res => {
setCharStats(res.data);
setCurrentName(name);
})
.catch(err => console.error(err))
}, [])
return (
<div>
<div>{ name }</div>
<button>Stats</button>
{ name === currentName ? charStats.map(char => {
return <Stats height={char.height} mass={char.mass} key={char.name} />;
}) : <h3>Loading...</h3>
}
</div>
)
}
The name prop I am getting from another component, I can console.log it and get each individual name so I know that works. But with the code above, I am getting the height and mass of every object returned instead of just the ones that match the name. How can I get specifically the height and mass of each object?
Looks like you might want to call filter before using map, like for example: data.filter(x => x.name === name).map(char => {.... which returns a collection that only contains the elements that match the condition). Or if you only want to find one element, its better to use .find(x => x.name === name) instead
I am trying to get only females from an array using a filter, but on the first attempt react query returns the whole array, after that it is working fine. Any idea what property I have to add or remove, so this side effect disappears.
Here is my code:
import React, { useState } from "react";
import { useQuery } from "react-query";
import getPersonsInfo from "../api/personCalls";
export default function Persons() {
const [persons, setPersons] = useState([]);
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onSuccess: (data) => {
setPersons(data.data);
},
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
const result = personData.data.filter(
(person) => person.gender === "female"
);
setPersons(result);
};
return (
<>
<button onClick={getFemaleOnlyHandler}>Female only</button>
{status === "loading" ? (
<div>Loading ... </div>
) : (
<div>
{persons.map((person) => (
<div>
<p>{person.name}</p>
<p>{person.lastName}</p>
<p>{person.address}</p>
<p>{person.gender}</p>
<p>{person.country}</p>
<p>{person.city}</p>
</div>
))}
</div>
)}
</>
);
}
I added the full code in code sandbox: https://codesandbox.io/s/relaxed-drake-4juxg
I think you are making the mistake of copying data from react-query into local state. The idea is that react-query is the state manager, so the data returned by react-query is really all you need.
What you are experiencing in the codesandbox is probably just refetchOnWindowFocus. So you focus the window and click the button, react-query will do a background update and overwrite your local state. This is a direct result of the "copy" I just mentioned.
What you want to do is really just store the user selection, and calculate everything else on the fly, something like this:
const [femalesOnly, setFemalesOnly] = React.useState(false)
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
setFemalesOnly(true)
};
const persons = femalesOnly ? personData.data.filter(person => person.gender === "female") : personData.data
you can then display whatever you have in persons, which will always be up-to-date, even if a background update yields more persons. If the computation (the filtering) is expensive, you can also use useMemo to memoize it (compute it only when personData or femalesOnly changes - but this is likely a premature optimization.
I'm not totally familiar with react-query however the problem is likely that it is re-fetching (async!) everytime the component updates. Since setPersons() triggers an update (ie. sets state) it'll update the new persons state to be the filtered female list and then trigger a fetch of all persons again which comes back and sets the persons state back to the full list (ie. see what happens when you click the female filter button and then just leave it).
There is a more idiomatic way to achieve this in React which is to keep a "single source of truth" (ie. all the persons) and dynamically filter that based on some local ui state.
For example see below where data becomes the source of truth, and persons is a computed value out of that source of truth. This has the benefit that if your original data changes you don't have to manually (read: imperatively) update it to also be females only. This is the "unidirectional data flow" and "reactivity" people always talk about and, honestly, it's what makes React, React.
const { data = { data: [] }, status } = useQuery(
"personsData",
getPersonsInfo,
{
onSuccess: (data) => {},
onError: (error) => {
console.log(error);
}
}
);
const [doFilterFemale, setFilterFemale] = useState(false);
const persons = doFilterFemale
? data.data.filter((person) => person.gender === "female")
: data.data;
https://codesandbox.io/s/vigorous-nobel-9n117?file=/src/Persons/persons.jsx
This is ofc assuming you are always just loading from a json file. In a real application setting, given a backend you control, I would always recommend implementing filtering, sorting and pagination on the server side otherwise you are forced to over-fetch on the client.
I loop through this array like this:
{props.choosenMovie.characters.map((characters) => (
<p>{characters}</p> /* This displays the URL of course */
))}
These URL's include a name object which is what i want to display,
what is the best practice to do this?
This is how it is displayed on my application, but the desire is to display the name object from the URL's.
In useEffect, map thru your array of urls and make the api call and store the promises in an array. Use promise.all and update the state which will cause re-render.
In render method map thru the updated state and display the names.
see working demo
Code snippet
export default function App() {
const [char, setChar] = useState([
"https://swapi.dev/api/people/1/",
"https://swapi.dev/api/people/2/"
]);
const [people, setPeople] = useState([]);
useEffect(() => {
const promiseArray = [];
char.forEach(c => {
promiseArray.push(fetch(c).then(res => res.json()));
Promise.all(promiseArray).then(res => {
console.log("res", res);
setPeople(res);
});
});
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{people.map((p, i) => {
return <p key={i}>{p.name}</p>;
})}
</div>
);
}
I was working with that API some time ago, and the way I approached it (to display the names etc) was with Promise.all
so the snipped looked like
axios.get(`https://swapi.dev/api/${this.props.match.path.split('/')[1]}/${this.props.match.params.id}/`).then((res) => {
let characters = []
// get all characters in the movie data
let characterPromises = []
res.data.characters.forEach((character) => {
characterPromises.push(axios.get(character))
})
// Create list with all characters names and link to page
Promise.all(characterPromises).then((res) => {
res.forEach((character, i) => {
characters.push(<li key={i}><Link to={`/${character.data.url.split('api/')[1]}`}>{character.data.name}</Link></li>)
})
this.setState({
characters
})
})
})
}
then I just used the characters lists (from state) in the render method
So I have been working hard for days and searching on how to do this. I have a Material UI table in my React App. I want to load a table where if my user has entries in the selected array it will prerender the checks in the DOM. The selected array is populated with the entries I want but my table which uses a onClick I think needs an event to trigger the DOM to render the check. This is relevant part of my table body.
<TableBody>
{this.props.competitorData.map(competitor => {
const isSelected = this.props.isSelected(competitor.key);
return (
<TableRow
hover
onClick={() => this.props.handleClick(competitor)}
role="checkbox"
aria-checked={isSelected}
tabIndex={-1}
key={competitor.key}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={isSelected} />
</TableCell>
I have a toggle that loads my table. It fills the selected the array with the subset of data I want trigger in componentWillMount. (it's 2 tables, tier1 and tier 2).
componentWillMount() {
this.renderChecks(this.props.team)
}
renderChecks(team) {
const { selected1 } = this.state;
const { selected2 } = this.state;
let newSelected1 = [];
let newSelected2 = [];
team.map(teammate => {
if (teammate.tier === "1") {
newSelected1 = newSelected1.concat(selected1, teammate.key)
} else if (teammate.tier === "2") {
newSelected2 = newSelected2.concat(selected2, teammate.key)
}
this.setState({ selected1: newSelected1 });
this.setState({ selected2: newSelected2 });
})
}
Essentially I need a way to render isSelected based of another list that is the smaller list (team is a subset of competitorData) that has the same keys. Ive tried so many things it's to many to list here. Im looking for help on what to do to make this work because nothing has worked and Im not sure what direction I should be going on in at this point. I've tried a lot of things that seem to cause instability in the render. Essentially I've tried to make the isSelected more state based but setting and resetting that state with inline functions like
{() => this.myFunctionThatUpdatesIsSelectedState(Key)}
These blow up in render. Sometimes cause an ugly infinite loop.
Update
Based on #Eld0w post below this does render my subset of checks.
checkKeys(val) {
return this.props.team.some(teammate => {
return val.key === teammate.competitorKey;
});
}
getCompetitors = () => {
const { competitorData, team } = this.props;
return competitorData.map(
value => ({
value,
isSelected: this.checkKeys(value)
})
)
}
Tables looks like this now.
<TableBody>
{this.getCompetitors().map(competitor => {
console.log('MYCOMPETITOR2::', competitor);
return (
<TableRow
hover
onClick={event => this.props.handleClick(event, competitor.value)}
role="checkbox"
aria-checked={competitor.isSelected}
tabIndex={-1}
key={competitor.value.key}
selected={competitor.isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={competitor.isSelected} />
</TableCell>
There is small issues I didn't see coming. Now my table renders only the preselected checks since im not using my previous isSelected function which was:
isSelected1 = key => this.state.selected1.indexOf(key) !== -1;
Basically i need to render the existing checks but maintain the standard isSelected function somewhere in the process as well. If I think of something or post anything about it I'll update here as well. Further input is obviously welcome.
I think i need to load my team into my selected array then run my standard isSelected function. But this is where I seem to run into trouble since that is state based. Render goes crazy on me.
Final Update
So it was late last night. I just needed to change the criterion to make this whole thing work. I load my team array in the local state selected array. Then performed isSelected property check on my competitor. Now it loads my preselected and the user can then edit selects in the table from that point.
Final Solution
Load the preselect team into the local selected state array.
componentWillMount() {
this.renderChecks(this.props.team);
}
I have tiered tables. That is just some business logic (not important here). teammate.competitorKey is the key I store that is same key as the larger table, which is competitorData. I need that to get the compares to work.
renderChecks(team) {
const { selected } = this.state;
let newSelected = [];
team.map(teammate => {
if (teammate.tier === '1') {
newSelected = newSelected.concat(selected, teammate.competitorKey)
this.setState({ selected: newSelected });
}
})
}
getCompetitor can now just verify the value key exist in the array using includes
getCompetitors = () => {
const { competitorData, team } = this.props;
console.log('THISSTATESELECTED:::', this.state.selected)
return competitorData.map(
value => ({
value,
isSelected: this.state.selected.includes(value.key)
})
)
}
And Final Table looks like
<TableBody>
{this.getCompetitors().map(competitor => {
return (
<TableRow
hover
onClick={event => this.handleClick(event, competitor.value)}
role="checkbox"
aria-checked={competitor.isSelected}
tabIndex={-1}
key={competitor.value.key}
selected={competitor.isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={competitor.isSelected} />
</TableCell>
I know this is a lot of writing but is spent a lot of time trying to get all this working. I hope it helps someone looking to do. I will look into making this more redux worth and possibly going the reselect route to optimize but for now im going to enjoy a working table for a day. Thank you again #Eld0w !!
So basically, you want to add an isSelected props to your competitors array depending on another array's values. Avoid using state, it's only props combinations.
The straightforward solution
Instead of mapping directly your competitor's array, map on a function returning an array.
getCompetitors = () => {
const { competitors, team } = this.props;
return competitors.map(
competitor => ({
...competitor,
isSelected: // criterion
})
)
}
Basically, what this does is destructuring the object and adding a new property isSelected thanks to the spread operator ... (ES6)
In your render then call this.getCompetitors().map(competitor => ...) instead of this.props.competitors.
Optimize this solution
You will want to use a plugin such as reselect to avoid any useless render operation.
const competitorSelector = createSelector(
props => props.competitors,
props => props.team,
(competitors, team) => competitors.map(
competitor => ({
...competitor,
isSelected: // criterion
})
)
)
and then use it like this in your render :
this.competitorSelector(this.props)
You will also need to use competitor.isSelected instead of isSelected since the property is now part of your competitor's properties.