Hello I am struggling to properly update my state from my child component to my parent.
Basically I am trying to set the current state to true onclick.
This is my parent component:
export default function Layout({ children }: Props) {
const [navigation, setNavigation] = useState([
{ name: 'Dashboard', href: '/', icon: HomeIcon, current: true },
{ name: 'Create Fact', href: '/facts/create', icon: UsersIcon, current: false },
{ name: 'Documents', href: '/documents', icon: InboxIcon, current: false }
])
return (
<>
<Sidebar navigation={navigation} setNavigation={setNavigation} />
This is my child Component (Sidebar)
type Props = {
navigation: Array<{
name: string
href: string
icon: any
current: boolean
}>
setNavigation: (
navigation: Array<{
name: string
href: string
icon: any
current: boolean
}>
) => void
}
const Sidebar = ({navigation, setNavigation}: Props) => {
const router = useRouter()
const toggleNavigation = (name: string) => {
// todo: Here I would like to properly update the state with the current selected navigation item (current)
const newNavigation = navigation.map(nav => {
if (nav.name === name) {
nav.current = true
return nav
}
})
}
return (
<nav className="flex-1 px-2 pb-4 space-y-1">
{navigation.map(item => (
<span
onClick={() => toggleNavigation(item.name)}
There are three problems:
You never call setNavigation with your new array.
You don't clear current on the formerly-current item.
Although you're creating a new array, you're reusing the objects within it, even when you change them, which is against the Do Not Modify State Directly rule.
To fix all three (see *** comments):
const toggleNavigation = (name: string) => {
const newNavigation = navigation.map(nav => {
if (nav.name === name) {
// *** #3 Create a *new* object with the updated state
nav = {...nav, current: true};
} else if (nav.current) { // *** #2 make the old current no longer current
nav = {...nav, current: false};
}
return nav;
});
// *** #1 Do the call to set the navigation
setNavigation(newNavigation);
};
Separately, though, I would suggest separating navigation out into two things:
The set of navigation objects.
The name of the current navigation item.
Then setting the navigation item is just setting a new string, not creating a whole new array with an updated object in it.
T.J. Crowder's solution and explanation are great.
Additionally, you can write that logic in a shorter syntax. Just a preference.
const newNavigation = navigation.map(nav => {
return nav.name === name
? { ...nav, current: true }
: { ...nav, current: false }
})
Related
I have a component called RightTab like this
const RightTab = ({ data }) => {
return (
<div className="RightTab flex__container " onClick={data.onClick}>
<img src={data.icon} alt="Dashboard Icon" />
<p className="p__poppins">{data.name}</p>
{data.dropDown === true ? (
<div className="dropdown__icon">
<img src={Assets.Arrow} alt="Arrow" />
</div>
) : (
<div className="nothing"></div>
)}
</div>
);
};
export default RightTab;
The tab has an active state in its CSS like this
.RightTab.active {
background-color: var(--primaryGreen);
}
as you have seen it changes the color when an active class is added. I have an array in the parent component that I pass down to the child component as props. Here is the array
const dataArray = [
{
name: "Dashboard",
icon: Assets.Dashboard,
dropDown: false,
onClick: handleDashBoardClick,
},
{
name: "Inventory",
icon: Assets.Inventory,
dropDown: true,
onClick: handleInventoryClick,
},
{
name: "Reports",
icon: Assets.Reports,
dropDown: true,
onClick: handleReportsClick,
},
];
Here is how I pass the props down.
<RightTab data={dataArray[0]} />
<RightTab data={dataArray[1]} />
<RightTab data={dataArray[2]} />
The data prop passed into the component is an object containing a function call as one of its properties like this. I have an onclick attribute on the child components' main container that is supposed to call the respective function.
The function is what adds the active class to make the background change color. However each time I click on the component it only changes the background of the first occurrence. And as you may have noticed I call the component thrice. No matter which component I click only the first ones background changes.
Here is an example of the function that is on the prop object.
const handleDashBoardClick = () => {
const element = document.querySelector(".RightTab");
element.classList.toggle("active");
};
I don't get what I'm doing wrong. What other approach can I use?
Although you use the component 3 times, it doesn't mean that a change you make in one of the components will be reflected in the other 2, unless you specifically use a state parameter that is passed to all 3 of them.
Also, the way you add the active class is not recommended since you mix react with pure js to handle the CSS class names.
I would recommend having a single click handler that toggles the active class for all n RightTab components:
const MainComponent = () => {
const [classNames, setClassNames] = useState([]);
const handleClick = (name) =>
{
const toggledActiveClass = classNames.indexOf('active') === -1
? classNames.concat(['active'])
: classNames.filter((className) => className !== 'active');
setClassNames(toggledActiveClass);
switch (name) {
case 'Dashboard';
// do something
break;
case 'Inventory':
// ....
break;
}
}
const dataArray = [
{
name: "Dashboard",
icon: Assets.Dashboard,
dropDown: false,
onClick: handleClick.bind(null, 'Dashboard'),
},
{
name: "Inventory",
icon: Assets.Inventory,
dropDown: true,
onClick: handleClick.bind(null, 'Inventory'),
},
{
name: "Reports",
icon: Assets.Reports,
dropDown: true,
onClick: handleClick.bind(null, 'Reports'),
},
];
return (
<>
{dataArray.map((data) =>
<RightTab key={data.name}
data={data}
classNames={classNames} />)}
</>
);
};
const RightTab = ({ data, classNames }) => {
return (
<div className={classNames.concat(['RightTab flex__container']).join(' ')}
onClick={data.onClick}>
<img src={data.icon} alt="Dashboard Icon" />
<p className="p__poppins">{data.name}</p>
{data.dropDown === true ? (
<div className="dropdown__icon">
<img src={Assets.Arrow} alt="Arrow" />
</div>
) : (
<div className="nothing"></div>
)}
</div>
);
};
I'm struggling while creating an element that is passed by the .map function. Basically, I want my webpage to create a div element with some date in it when a button is clicked for that I'm using a .map function but it isn't working out.
const handleSubmit = (e) => {
e.preventDefault();
const data = {title:`${title}`, desc:`${desc}`, date:`${date}`};
data.map(userinfo =>{
return(<div>
<h1>{userinfo.title}</h1>
</div>)
})
console.log(data);
}
In reactJS, if we want to display our data in HTML webpage we usually do that in the render funciton.
We can use userInfo variable in the state object.
The userInfo data is hardcoded for demonstration purposes but you can also populate the userInfo variable either using API or in any other way you like.
Moreover, showUserInfo is another variable (initially false) that would render the data once it is set to true
this.state = {
userInfo: [
{
title: 'one',
desc: '',
date: new Date()
},
{
title: 'two',
desc: '',
date: new Date()
}
],
showUserInfo: false
}
On a click event we can set showUserInfo to true using setState function.
more on setState function via this link ->
https://medium.com/#baphemot/understanding-reactjs-setstate-a4640451865b
handleSubmit = async (event) => {
event.preventDefault();
this.setState(
{
...this.state,
showUserInfo: true
}
)
}
In the render function, if showUserInfo is false then userInfo.map is never going to render unless showUserInfo is set to true which we do using a click listener that is associated with our function handleSubmit.
render(){
return (
<div>
<button onClick={this.handleSubmit}>Click Me</button>
{ this.state.showUserInfo &&
this.state.userInfo.map(item =>(
<div>
<p> {item.date.toString()} </p>
</div>
) ) }
</div>
);
}
Overall the result looks a something like this.
export default class App extends React.Component {
constructor() {
super();
this.state = {
showUserInfo: false,
userInfo: [
{
title: 'one',
desc: '',
date: new Date()
},
{
title: 'two',
desc: '',
date: new Date()
}
],
}
}
handleSubmit = async (event) => {
event.preventDefault();
this.setState(
{
...this.state,
showUserInfo: true
}
)
}
render(){
return (
<div>
<button onClick={this.handleSubmit}>Click Me</button>
{ this.state.showUserInfo &&
this.state.userInfo.map(item =>(
<div>
<p> {item.date.toString()} </p>
</div>
) ) }
</div>
);
}
}
For some reason my React component seems to remember its old state when going to another tab and back again, instead of reloading completely.
Basically when I click on the "Create" tab in my navbar and back to the "Board" tab data is populated twice instead of once, see image below. When going back the Board component this.state has two of each taskIds, as if it the component state still had the data from the initial page load when loading again. I have a React component looking like this:
const columnOrder = ['todo', 'in-progress', 'in-review', 'done']
const EMPTY_COLUMNS = {
'todo': {
id: 'todo',
title: 'TODO',
taskIds: []
},
'in-progress': {
id: 'in-progress',
title: 'In Progress',
taskIds: [],
},
'in-review': {
id: 'in-review',
title: 'In Review',
taskIds: []
},
'done': {
id: 'done',
title: 'Done',
taskIds: []
}
};
export class Board extends Component {
constructor(props) {
super(props);
this.onLoadEpic = this.onLoadEpic.bind(this);
this.state = {
columnOrder: columnOrder,
columns: {
'in-progress': {
id: 'in-progress',
title: 'In Progress',
taskIds: [],
},
// ...more columns similar to above
},
};
// Load state data on mount
componentDidMount() {
loadEpic(arg1, arg2);
}
// Async function loading items from DB and formatting into useful columns
async loadEpic(arg1, arg2) {
axios.get(...)
.then((response) => {
let data = response.data;
let newTasks = {};
let newColumns = EMPTY_COLUMNS;
data.taskItems.forEach(function(item) {
let id = item.id.toString();
newColumns[item.status]["taskIds"].push(id);
newTasks[id] = {
...item,
id: id
}
});
this.setState({
tasks: newTasks,
columns: newColumns
});
})
}
render() {
// Prints ["7"] on initial load and ["7", "7"] after going back and forth
console.log(this.state.columns["in-progress"].taskIds);
return (
// Simplified, but this is the main idea
<Container>
<DragDropContext onDragEnd={this.onDragEnd}>
{
this.state.columnOrder.map((columnId) => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId]
return (
<Column key={column.id} column={column} tasks={tasks}/>
)
}
}
</DragDropContext>
</Container>
)
}
}
and an App.js with Routing looking like this:
export default class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={Board} />
<Route exact path='/create' component={Create} />
</Layout>
);
}
}
Okay, so I figured it out: it's the EMPTY_COLUMNS constant that is bugging out. When the component is re-rendered, the same EMPTY_COLUMNS object is referenced - so the constant is being appended to. Instead, I should make a copy of the empty columns:
// Before - same object is being appended to, doesn't work
let newColumns = EMPTY_COLUMNS;
// After - create a deep copy of the constant, does work
let newColumns = JSON.parse(JSON.stringify(EMPTY_COLUMNS));
I am just learning to program and am writing one of my first applications in React. I am having trouble with an unexpected mutation that I cannot find the roots of. The snippet is part of a functional component and is as follows:
const players = props.gameList[gameIndex].players.map((item, index) => {
const readyPlayer = [];
props.gameList[gameIndex].players.forEach(item => {
readyPlayer.push({
id: item.id,
name: item.name,
ready: item.ready
})
})
console.log(readyPlayer);
readyPlayer[index].test = "test";
console.log(readyPlayer);
return (
<li key={item.id}>
{/* not relevant to the question */}
</li>
)
})
Now the problem is that readyPlayer seems to be mutated before it is supposed to. Both console.log's read the same exact thing. That is the array with the object inside having the test key as "test". forEach does not mutate the original array, and all the key values, that is id, name and ready, are primitives being either boolean or string. I am also not implementing any asynchronous actions here, so why do I get such an output? Any help would be greatly appreciated.
Below is the entire component for reference in its original composition ( here also the test key is replaced with the actual key I was needing, but the problem persists either way.
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
// import styles from './Lobby.module.css';
const Lobby = ( props ) => {
const gameIndex = props.gameList.findIndex(item => item.id === props.current.id);
const isHost = props.gameList[gameIndex].hostId === props.user.uid;
const players = props.gameList[gameIndex].players.map((item, index) => {
const isPlayer = item.id === props.user.uid;
const withoutPlayer = [...props.gameList[gameIndex].players];
withoutPlayer.splice(index, 1);
const readyPlayer = [];
props.gameList[gameIndex].players.forEach(item => {
readyPlayer.push({
id: item.id,
name: item.name,
ready: item.ready
})
})
const isReady = readyPlayer[index].ready;
console.log(readyPlayer);
console.log(!isReady);
readyPlayer[index].ready = !isReady;
console.log(readyPlayer);
return (
<li key={item.id}>
{isHost && index !== 0 && <button onClick={() => props.updatePlayers(props.gameList[gameIndex].id, withoutPlayer)}>Kick Player</button>}
<p>{item.name}</p>
{isPlayer && <button onClick={() =>props.updatePlayers(props.gameList[gameIndex].id, readyPlayer)}>Ready</button>}
</li>
)
})
let showStart = props.gameList[gameIndex].players.length >= 2;
props.gameList[gameIndex].players.forEach(item => {
if (item.ready === false) {
showStart = false;
}
})
console.log(showStart);
return (
<main>
<div>
{showStart && <Link to="/gameboard" onClick={props.start}>Start Game</Link>}
<Link to="/main-menu">Go back to Main Menu</Link>
</div>
<div>
<h3>Players: {props.gameList[gameIndex].players.length}/4</h3>
{players}
</div>
</main>
);
}
Lobby.propTypes = {
start: PropTypes.func.isRequired,
current: PropTypes.object.isRequired,
gameList: PropTypes.array.isRequired,
updatePlayers: PropTypes.func.isRequired,
user: PropTypes.object.isRequired
}
export default Lobby;
Note: I did manage to make the component actually do what it is supposed, but the aforementioned unexpected mutation persists and is still a mystery to me.
I have created a basic working example using the code snippet you provided. Both console.log statements return a different value here. The first one returns readyPlayer.test as undefined, the second one as "test". Are you certain that the issue happens within your code snippet? Or am I missing something?
(Note: This answer should be a comment, but I am unable to create a code snippet in comments.)
const players = [
{
id: 0,
name: "John",
ready: false,
},
{
id: 1,
name: "Jack",
ready: false,
},
{
id: 2,
name: "Eric",
ready: false
}
];
players.map((player, index) => {
const readyPlayer = [];
players.forEach((item)=> {
readyPlayer.push({
id: item.id,
name: item.name,
ready: item.ready
});
});
// console.log(`${index}:${readyPlayer[index].test}`);
readyPlayer[index].test = "test";
// console.log(`${index}:${readyPlayer[index].test}`);
});
console.log(players)
I have a header component where I need to render three buttons, so every three buttons have three props. One is the class name, click handler and text.
So out of three buttons, two buttons act as a toggle button, so based on the click the text should change.
See the below code:
class App extends Component(){
state = {
navigationList: [{
text: 'Signout',
onClickHandler: this.signoutHandler,
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode,
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay,
customClassName: 'buttonStyle'
}]
}
signoutHandler = () => {
// some functionality
}
viewMode = () => {
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay = () => {
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<Header navigationList={this.state.navigationList}/>
)
}
}
const Header = ({navigationList}) => {
return (
<>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</>
)
}
The other way is I can pass all the props one by one and instead of an array I can write three button elements render it, but I am thinking to have an array and render using a map.
So which method is better, the problem that I am facing is if use the array. map render
the approach I need to set the initial value as a variable outside and how can I set the state.
And I am getting the onClick method is undefined, is it because the function is not attached to the state navigation list array.
Update
I declared the functions above the state so it was able to call the function.
So in JS, before the state is declared in the memory the functions should be hoisted isn't.
class App extends React.Component {
constructor(props){
super();
this.state = {
isStudents:false,
activeWay:false,
}
}
createList(){
return [{
text: 'Signout',
onClickHandler: this.signoutHandler.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay.bind(this),
customClassName: 'buttonStyle'
}];
}
signoutHandler(){
}
viewMode(){
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay(){
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<div>
<div>ddd</div>
<Header navigationList={this.createList()} />
</div>
)
}
}
const Header = ({navigationList}) => {
console.log(navigationList);
return (
<div>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
https://jsfiddle.net/luk17/en9h1bpr/
Ok I will try to explain, If you see you are using function expressions in your class and as far as hoisting is concerned in JavaScript, functions expressions are not hoisted in JS only function declarations are hoisted, function expressions are treated as variables in JS.
Now for your case you don't have to shift your functions above the state, you can simply use constructor for initializing state as
constructor(props) {
super(props);
this.state = {
isStudents: false,
activeWay: false,
navigationList: [
{
text: "Signout",
onClickHandler: this.signoutHandler,
customClassName: "buttonStyle"
},
{
text: "Teachers",
onClickHandler: this.viewMode,
customClassName: "buttonStyle"
},
{
text: "Active Hidden",
onClickHandler: this.activeWay,
customClassName: "buttonStyle"
}
]
};
}
Now you will have your handlers available as it is
Sandbox with some modification just to show
EDIT:
You can have default text for buttons and change it when clicking,
Sandbox updated
Hope it helps