Invalid hooks calls in React - reactjs

I have a problem cause I'm getting the userAccess from redux. I need to identify if it is admin or not.
My problem is that its not inside a hook so I can't get the redux value.
here's my code below
Code
import { useSelector } from 'react-redux';
const role = useSelector((state) => state.role);
export default [
{
id: 1,
text: 'Leaders',
url: `/leaders`,
userAccess: true,
},
{
id: 2,
text: 'Users',
url: `/users`,
userAccess: role === 'Admin',
},
];
Code
const Menu = ({ items, isCollapseMenu }) => {
const classes = useStyles();
return (
<div className={classes.root}>
{items.map(
(item) =>
item.userAccess && (
<MenuItem
key={item.id}
title={item.title}
icon={item.icon}
text={item.text}
shortText={item.shortText}
url={item.url}
submenu={item.submenu}
isCollapseMenu={isCollapseMenu}
/>
)
)}
</div>
);
};

You are correct, the useSelector hook can't be used on it's own outside a React functional component or custom React hook.
I suggest you configure your exported menu items to either set userAccess to a boolean or role value, and access your redux state in the Menu component.
Menu items
export default [
{
id: 1,
text: 'Leaders',
url: `/leaders`,
userAccess: true,
},
{
id: 2,
text: 'Users',
url: `/users`,
userAccess: 'Admin',
},
];
Menu
import { useSelector } from 'react-redux';
const checkAccess = (access, role) => {
if (typeof access === 'string') {
return role === access;
};
return access;
};
const Menu = ({ items, isCollapseMenu }) => {
const classes = useStyles();
const role = useSelector((state) => state.role);
return (
<div className={classes.root}>
{items.map(
(item) =>
checkAccess(item.userAccess, role) && (
<MenuItem
key={item.id}
title={item.title}
icon={item.icon}
text={item.text}
shortText={item.shortText}
url={item.url}
submenu={item.submenu}
isCollapseMenu={isCollapseMenu}
/>
)
)}
</div>
);
};

You need to run useSelector() in a component or in another hook. You can create a custom hook (useRoles) that returns the array of roles, and use it in the component:
const useRoles = () => {
const role = useSelector((state) => state.role);
return [{
id: 1,
text: 'Leaders',
url: `/leaders`,
userAccess: true,
},
{
id: 2,
text: 'Users',
url: `/users`,
userAccess: role === 'Admin',
},
];
};
Get the items from the useRoles hook:
const Menu = ({ isCollapseMenu }) => {
const items = useRoles();
const classes = useStyles();
return (
<div className={classes.root}>
{items.map(
(item) =>
item.userAccess && (
<MenuItem
key={item.id}
title={item.title}
icon={item.icon}
text={item.text}
shortText={item.shortText}
url={item.url}
submenu={item.submenu}
isCollapseMenu={isCollapseMenu}
/>
)
)}
</div>
);
};

Related

Cannot pass selected date from MUI DateTimePicker into SWR hook

I have a component that holds a MUI DataGrid.
Now, for any row of the DataGrid I need to render a DateTimePicker column. There is data coming
in with a SWR call which data could contain a datetime for the column or not.
mainPage/sdk.js
export const useSomeData = (
params: TUseFetchOptions['params']
) => {
const { data, isLoading, mutate } = useFetch<TPagination<TSomeData>>('/some-data/');
const approve = useCallback(
(someData: TSomeData) => {
const data = { published_at: someData.published_at }
return requestHandler(
post(`/some-data/update/`, data)
)
.then((someData) => {
mutate((prev) => {
if (!prev) {
return prev;
}
// some irrelevant mutation for swr caching happens here
return prev;
}
return prev;
}, false);
})
.catch((error) =>
// some irrelevant alerting happens here
);
},
[mutate]
);
return useMemo(
() => ({ someData: data, isLoading, approve,mutate }),
[data, isLoading, mutate, approve]
);
};
mainPage/index.tsx
import {useSomeData} from './sdk'
const SomeDataPublish = () => {
// const {params} = ....
// const dataGridProps = ...
const { someData, isLoading, approve } = useSomeData(params);
return (
<Stack>
{someData && (
<SomeDataDataGrid
someData={someData}
params={params}
DataGridProps={dataGridProps}
handleApprove={approve}
/>
)}
</Stack>
);
};
export default SomeDataPublish;
mainPage/componenets/someDataDataGrid.tsx
export const columns: GridColumns = [
{
// some field
},
{
// some field
},
{
// some field
},
// ...
];
const buildColumnsData = (
handleApprove: ReturnType<typeof useSomeData>['approve'],
): GridColumns => {
return [
...columns,
{
field: 'published_at',
headerName: 'Publish at',
flex: 0.75,
renderCell: (params: any) => <RowDatePicker params={params} />
},
{
field: '',
type: 'actions',
flex: 0.4,
getActions: (params: any) => [
<RowActions
params={params}
handleApprove={handleApprove}
/>
]
}
];
};
const buildRows = (someData: TSomeData[]): GridRowsProp => {
return someData.map((row) => ({
id: row.id,
// ...
published_at: _.get(row, 'published_at'),
}));
};
const SomeDataDataGrid: FC<{
someData: TPagination<TSomeData>;
params: TUseFetchOptions['params'];
DataGridProps: Partial<MuiDataGridProps>;
handleApprove: ReturnType<typeof useSomeData>['approve'];
}> = ({ someData, params, DataGridProps, handleApprove }) => {
return (
<Paper>
<DataGrid
// ...
columns={buildColumnsData(handleApprove)}
rows={someData ? buildRows(someData.results) : []}
// ...
{...DataGridProps}
/>
</Paper>
);
};
export default SomeDataDataGrid;
mainPage/componenets/rowDatePicker.tsx
const RowDatePicker: React.FC<{
params: GridRowParams;
}> = ({ params }) => {
const [publishedAt, setPublishedAt] = React.useState(params.row.published_at);
return (
<>
<DateTimeField
label={'Pick Date'}
value={publishedAt}
onChange={setPublishedAt}
/>
</>
);
};
export default RowDatePicker;
mainPage/componenets/rowAction.tsx
const RowActions: React.FC<{
params: GridRowParams;
handleApprove: ReturnType<typeof useSomeData>['approve'];
}> = ({ params, handleApprove }) => {
return (
<>
<Tooltip title="Approve">
<IconButton
color="success"
disabled={false}
onClick={(e) => {
console.log(params.row)}
handleApprove(params.row)
>
<AppIcons.CheckCircle />
</IconButton>
</Tooltip>
</>
);
};
export default RowActions;
The problem that I have - if I change the date from the date picker, on clicking the <AppIcons.CheckCircle /> in the RowActions component I expect the row.published_at to be updated with the new value. Then I pass the new updated object (with the updated published_at attribute) to the handleApprove hook so I can make some mutations and pass the updated object (with new published_at value) to the back end.
However, on examining the someData object that is passed to the approve hook the published_at field has its old value ( the one that came from the SWR fetcher).
I know that I need to mutate somehow params.row.published_at = publishedAt in the onChange callback of the RowDatePicker.DateTimePicker, but I am not sure how to do it. Any help would be appreciated.

How to properly change the boolean inside of object in array?

So, I'm trying toggle the Icon based on the isBadData per email data in the object of array. But I can't seem to find out how could save it back to the state so it can update the Icon image in LeadProfileComponent.
This is what it looks like:
checkIcon = isBadData: false
crossIcon = isBadData: true
Heres my code:
// ModalComponent.js
const [leadProfile, setLeadProfile] = useState([
{
id: 'd114877b-074b-4aa2-a3f0-3b9446885336',
firstName: 'wqe',
lastName: 'wqe',
name: 'wqe wqe',
email: [
{
type: 'personal',
address: 'qwe#hotmail.com',
valid_since: '2010-05-09',
isBadData: true,
},
{
type: 'personal',
address: 'wqe#hotmail.com',
valid_since: '2017-03-09',
isBadData: true,
},
{
type: 'personal',
address: 'wqe#aol.com',
valid_since: '2009-01-12',
isBadData: true,
},
],
},
]);
<LeadProfileComponent leadProfile={leadProfile} setLeadProfile={setLeadProfile} />
// LeadProfileComponent.js
const LeadProfileComponent = (props) => {
const handleChildEmail = (email, index) => {
props.setLeadProfile((prev: any) => {
const value = { ...prev[0].email[index] };
console.log('inside value');
console.log(value);
value.isBadData = !value.isBadData;
console.log(value);
// return prev;
return [value];
});
console.log('props.leadProfile');
console.log(props.leadProfile);
};
return (
<>
{
props.leadProfile.map((lead, index) => (
return(
<>
{lead.email.map(() => {
return (
<button
id="btnCheck"
onClick={() => {
handleChildEmail(email, index);
}}
>
<img
src={
email.isBadData !== true
? checkIcon
: closeIcon
}
/>
</button>
)
})}
</>
)
}
</>
);
}
Heres what it looks like when you console log inside of handChildEmail function:
As you can see, I was able to change the inside boolean of email[0], but I cant save it back to the leadProfile state since I have a missing part in the destructuring part
Break your components in smaller parts, and manage each email individually
LeadProfileEmailComponent.js
const LeadProfileEmailComponent = ({ initialEmailData, ...props }) => {
const [emailData, setEmailData] = useState(initialEmailData);
return (
<button
id="btnCheck"
onClick={() => {
setEmailData({
...emailData,
isBadData: !emailData.isBadData
});
}}
>
<img
src={
emailData.isBadData !== true
? checkIcon
: closeIcon
}
/>
</button>
)
}
Change this in LeadProfileComponent:
{lead.email.map((email) => {
return (
<LeadProfileEmailComponent initialEmailData={email} />
)
})}
The downside is, the state of the parent component will not be updated. However this is standard design pattern practise, you should not rely on the parent component data for this.

Can I change a element state in react without changing every element state?

im making a portfolio website and have multiple different buttons with skills which contain an img and p tag. I want to show the description of each tag everytime a user clicks on the button. how can I do this? right now everytime user clicks it, all buttons show description.
const Skills = () => {
const [state, setState] = useState(false)
let skills = [
{ id: 1, desc: 'HTML5', state: false, img: htmlIcon },
{ id: 2, desc: 'CSS3', state: false, img: cssIcon },
{ etc....}
const showDesc = (id) => {
console.log(skills[id-1] = !state);
setState(!state)
}
return (
{skills.map(skill => (
<button onClick={(id) => showDesc(skill.id)}>
<img style={ state ? {display:'none'} : {display:'block'}} src={skill.img} />
<p style={ state ? {display:'block'} : {display:'none'}}>{skill.desc}</p>
</button>
))}
I recommend to manipulate element state instead of entry list. But if you really need to manipulate entry list you should add that list to your state. Then when you want to show/hide specific item, you need to find that item in state and correctly update entry list by making a copy of that list (with updated item). For example you can do it like this:
import React, { useState } from 'react';
const Skills = () => {
const [skills, setSkills] = useState([
{
id: 1,
desc: 'HTML5',
state: false,
img: htmlIcon, // your icon
},
{
id: 2,
desc: 'CSS3',
state: false,
img: cssIcon, // your icon
},
]);
const showDesc = (id) => {
const newSkills = skills.map((item) => {
if (item.id === id) {
return {
...item,
state: !item.state,
};
}
return item;
});
setSkills(newSkills);
};
return (
<div>
{skills.map(({
id,
img,
state,
desc,
}) => (
<button type="button" key={id} onClick={() => showDesc(id)}>
<img alt="img" style={state ? { display: 'none' } : { display: 'block' }} src={img} />
<p style={state ? { display: 'block' } : { display: 'none' }}>{desc}</p>
</button>
))}
</div>
);
};
Instead of manipulating all list, you can try to move show/hide visibility to list item itself. Create separate component for item and separate component for rendering that items. It will help you to simplify logic and make individual component responsible for it visibility.
About list rendering you can read more here
For example you can try something like this as alternative:
import React, { useState } from 'react';
const skills = [
{
id: 1,
desc: 'HTML5',
img: htmlIcon, // your icon
},
{
id: 2,
desc: 'CSS3',
img: cssIcon, // your icon
},
];
const SkillItem = ({
img,
desc = '',
}) => {
const [visibility, setVisibility] = useState(false);
const toggleVisibility = () => {
setVisibility(!visibility);
};
const content = visibility
? <p>{desc}</p>
: <img alt="img" src={img} />;
return (
<div>
<button type="button" onClick={toggleVisibility}>
{content}
</button>
</div>
);
};
const SkillList = () => skills.map(({
id,
img,
desc,
}) => <SkillItem img={img} desc={desc} key={id} />);

Dynamic dropdown menus react

I am trying to implement a dynamic dropdown menu. Clicking on the add button will show a dropdown menu that allow users to select an item, and each dropdown menu has the same list of options. I have the dropdown options store in an array, and clicking the add button will increment another array of options to the array
The issues I am having now is that, clicking the remove button doesn’t reflect what I have removed on the UI. For example, if I remove the first dropdown, it reflects that the second one is deleted.
import React, { useState } from "react";
const disciplines_fake_data = [
{ name: "discipline1", id: 0 },
{ name: "discipline2", id: 1 },
{ name: "discipline3", id: 2 },
{ name: "discipline4", id: 3 },
{ name: "discipline5", id: 4 },
{ name: "discipline6", id: 5 },
{ name: "discipline7", id: 6 },
{ name: "discipline8", id: 7 }
];
export default function Discipline({
registration,
handleRemoveDisciplineClick,
handleSelectDisciplineClick
// handleInputChange,
}) {
const [disciplinesDropdowns, setDisciplinesDropdowns] = useState([]);
const handleAddDisciplineClick = () => {
setDisciplinesDropdowns((prev) => [...prev, disciplines_fake_data]);
};
const handleRemoveDropdownClick = (index) => {
const newDisciplinesDropdowns = [...disciplinesDropdowns];
newDisciplinesDropdowns.splice(index, 1);
setDisciplinesDropdowns([...newDisciplinesDropdowns]);
handleRemoveDisciplineClick(`otherDisciplines_${index + 1}`);
};
return (
<div>
<div>
{disciplinesDropdowns.length > 0 &&
disciplinesDropdowns.map((disciplines, index) => (
<div style={{ marginTop: "10px" }} key={index}>
<article>
<label htmlFor={`otherDisciplines_${index + 1}`}>
Discipline {index + 1}
</label>
<button
onClick={(e) => {
e.preventDefault();
handleRemoveDropdownClick(index);
}}
>
REMOVE
</button>
</article>
<select
defaultValue="choose from all disciplines"
name={`otherDisciplines_${index + 1}`}
onChange={handleSelectDisciplineClick}
// onChange={handleInputChange}
>
<option disabled value="choose from all disciplines">
-choose from all disciplines-
</option>
{disciplines.map((discipline) => (
<option key={discipline.id} value={discipline.name}>
{discipline.name}
</option>
))}
</select>
</div>
))}
<div style={{ marginTop: "20px" }}>
<button
onClick={(e) => {
e.preventDefault();
handleAddDisciplineClick();
}}
>
<span> add another discipline</span>
</button>
</div>
</div>
</div>
);
}
import React, { useState, useReducer, useEffect } from "react";
import _ from "lodash";
import Discipline from "./Discipline";
const initialState = {
otherDisciplines: []
};
const FORM_ACTION = {
SELECT_DISCIPLINES: "select more disciplines",
REMOVE_DISCIPLINES: "remove disciplines"
};
function registrationReducer(state, action) {
switch (action.type) {
case FORM_ACTION.SELECT_DISCIPLINES:
const name = action.payload.name;
const value = action.payload.value;
const newDisciplines = [
...state.otherDisciplines,
{
[name]: value
}
];
newDisciplines.map((discipline) => {
if (discipline[name]) {
discipline[name] = value;
}
});
return {
...state,
otherDisciplines: _.uniqWith(newDisciplines, _.isEqual)
};
case FORM_ACTION.REMOVE_DISCIPLINES:
return {
...state,
otherDisciplines: state.otherDisciplines.filter(
(discipline) => Object.keys(discipline)[0] !== action.payload
)
};
default:
return { ...state, [action.input]: action.value };
}
}
export default function App() {
const [registration, dispatch] = useReducer(
registrationReducer,
initialState
);
console.log(registration);
const handleInputChange = ({ target }) => {
const { name, value } = target;
const action = {
input: name,
value: value
};
dispatch(action);
};
return (
<form
// onSubmit={handleFormSubmit}
>
<div>
<Discipline
registration={registration}
handleInputChange={handleInputChange}
handleSelectDisciplineClick={(e) => {
const { name, value } = e.target;
dispatch({
type: FORM_ACTION.SELECT_DISCIPLINES,
payload: { name, value }
});
}}
handleRemoveDisciplineClick={(discipline) => {
dispatch({
type: FORM_ACTION.REMOVE_DISCIPLINES,
payload: discipline
});
}}
/>
);
</div>
</form>
);
}
Using the list index as an identifier for the element is not recommended.
Instead of list (disciplinesDropdowns) you can make use of dictionary object to store dropdowns with unique identifiers and pass those unique identifiers to "handleRemoveDropdownClick".
Can have a function, that generates random and unique key before adding dropdowns to "disciplinesDropdowns".

React hooks: Update an object value within an array in state

What's the best approach to update the values of objects within an array in the state? Can't really wrap my head around hooks yet. The class approach seems to be way clearer for me at least in this case
In the below situation I'd like to change the active value on click to false within the object and also add a date value of when that happened.
handleChangeStatus doesn't work at all, I just get the 'test' on click, no errors.
const App = () => {
const [tasks, setTasks] = useState([
{
text: 'Example 1',
id: 1,
urgent: true,
targetDate: '2021-07-16',
active: true,
finishDate: null,
},
{
text: 'Example 2',
id: 2,
urgent: false,
targetDate: '2021-06-03',
active: false,
finishDate: null,
},
{
text: 'Example 3',
id: 3,
urgent: false,
targetDate: '2021-07-16',
active: true,
finishDate: null,
},
]);
const handleChangeStatus = (id) => {
console.log('test');
const newArr = [...tasks];
newArr.forEach((task) => {
if (task.id === id) {
console.log(task.id);
task.active = false;
task.finishDate = new Date().getTime();
}
});
setTasks(newArr);
};
return (
<div className="App">
<AddTask />
<TaskList tasks={tasks} changeStatus={handleChangeStatus} />
</div>
);
};
TaskList
const TaskList = (props) => {
const active = props.tasks.filter((task) => task.active);
const done = props.tasks.filter((task) => !task.active);
const activeTasks = active.map((task) => (
<Task key={task.id} task={task} changeStatus={props.changeStatus} />
));
const doneTasks = done.map((task) => <Task key={task.id} task={task} />);
return (
<>
<h3>Active Tasks ({active.length})</h3>
<ul>{activeTasks}</ul>
<hr />
<h3>Done Tasks ({done.length})</h3>
<ul>{doneTasks}</ul>
</>
);
};
Task
const Task = (props) => {
const { text, id, urgent, targetDate, active } = props.task;
const style = { color: 'red' };
if (active) {
return (
<p>
<strong style={urgent ? style : null}>{text}</strong>, id: {id}, target
date: {targetDate} <button onClick={props.changeStatus}>Done</button>
</p>
);
} else {
return (
<p>
<strong style={urgent ? style : null}>{text}</strong>, id: {id}, target
date: {targetDate}
</p>
);
}
};
<button onClick={props.changeStatus}>Done</button>
You are sending event object to the function, try sending id
<button onClick={() => props.changeStatus(id)}>Done</button>
Per the React Docs
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
so you could do something like:
const handleChangeStatus = (id) => {
console.log('test');
setTask((prev)=>prev.map((task)=>{
if(task.id === id){
return {...task,active: false, finishDate: new Date().getTime()}
}
else{
return task;
}
})
}

Resources