I have trouble debugging this code.
I have an App component:
function App() {
const [state, setState] = useState(0);
const onSelectItem = () => {
console.log("🐞: onSelectItem -> currentState", state);
};
// items is an array of ReactNode: button, when click on it. It will log the currentState.
const items = ["FirstItem", "SecondItem"].map(item => (
<button key={item} onClick={() => onSelectItem()}>
{item}
</button>
);
);
return (
<div className="App">
<Menu items={items} />
<hr />
<button onClick={() => setState(prevState => prevState + 1)}>Change State</button>
</div>
);
}
My Menu components will receive items prop, and render it. It also has ability to set the active item. For simplicity's sake, I render a button to set activeItem to the first one. The active item will also be rendered.
function Menu({ items }) {
const [activeItem, setActiveItem] = useState(items[0]);
return (
<div>
{items}
<hr />
{activeItem}
</div>
);
}
Now, come to the main part:
I press the button (before hr) => it shows currentState (OK)
I press the active button (after hr) => it shows currentState (OK)
I press change state button => the state now changes to 1 (OK)
Now, if I press the button (before hr ) => It shows currentState is 1 (OK)
But, if I press the active button (after hr ) => It still shows 0 (which is the last state) (???)
My guess is React keeps remembering everything when using useState. But I'm not sure. Could anyone explain this for me!
I also include the snippets for you to easily understand my problem.
const {useState} = React;
function Menu({ items }) {
const [activeItem, setActiveItem] = useState(items[0]);
return (
<div>
{items}
<hr />
<span>Active Item:</span>
{activeItem}
</div>
);
}
function App() {
const [state, setState] = useState(0);
console.log(state);
const onSelectItem = () => {
console.log("🐞: onSelectItem -> currentState", state);
};
const items = ["FirstItem", "SecondItem"].map(item => {
return (
<button key={item} onClick={() => onSelectItem()}>
{item}
</button>
);
});
return (
<div className="App">
<Menu items={items} />
<hr />
<button onClick={() => setState(prevState => prevState + 1)}>Change State</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You are trying to access the state from App component in your Menu component.
State is local to that component and can't be accessed outside, if you would like to access the state outside the component you can refer to the useContext hook implementation.
https://reactjs.org/docs/hooks-reference.html#usecontext
Reason you are seeing 0 in the Active state is that is the default value of useState.
You need to pass key to your menu component.
Whenever there is change in props, the component has to re-render with new props.
Refer this artcile from their official docs - https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
Change I made is passing state as key to Menu component
const {useState} = React;
function Menu({ items }) {
const [activeItem, setActiveItem] = useState(items[0]);
return (
<div>
{items}
<hr />
<span>Active Item:</span>
{activeItem}
</div>
);
}
function App() {
const [state, setState] = useState(0);
console.log(state);
const onSelectItem = () => {
console.log("🐞: onSelectItem -> currentState", state);
};
const items = ["FirstItem", "SecondItem"].map(item => {
return (
<button key={item} onClick={() => onSelectItem()}>
{item}
</button>
);
});
return (
<div className="App">
<Menu items={items} key={state}/>
<hr />
<button onClick={() => setState(prevState => prevState + 1)}>Change State</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Related
Problem: when i click on the button
<button
onClick={() => {
navigate('/posts');
setResponse(e.id);
}}
>
I get this error: Warning: Cannot update a component (Home) while rendering...
I think problem only in this line
navigate('/posts');
because if I delete it, error disappears
full code under without import
App.js
function App() {
const [response, setResponse] = useState({});
return (
<Router>
<Routes>
<Route exact path="/" element={<Home setResponse={setResponse} />} />
<Route exact path="/posts" element={<Posts response={response} />} />
<Route exact path="*" />
</Routes>
</Router>
);
}
export default App;
Home.js
function Home({ setResponse }) {
const [modal, setModal] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const state = useSelector((state) => state);
console.log(state);
if (state.user.isLoading) {
return <h1>Loading...</h1>;
}
const toggleModal = () => {
setModal(!modal);
};
return (
<div className="App">
<button onClick={(e) => dispatch(fetchUsers())}>Fetch users</button>
{state.user.data &&
state.user.data.map((e) => (
<div key={e.id}>
<li key={e.name}>{e.name}</li>
<button
key={e.id + 10}
onClick={() => {
navigate('/posts');
setResponse(e.id);
}}
className="btn"
>
Posts
</button>
<button onClick={toggleModal} key={e.id + 100} className="bnt">
Albums
</button>
</div>
))}
<Albums modal={modal} setModal={setModal} />
</div>
);
}
export default Home;
Posts.js
function Posts({ response }) {
const dispatch = useDispatch();
const navigate = useNavigate();
const state = useSelector((state) => state);
console.log(state);
if (state.post.isLoading) {
return <h1>Loading...</h1>;
}
if (!state.post.data) {
dispatch(fetchPosts());
}
return (
<div className="App">
Posts
{state.post.data &&
state.post.data
.filter((e) => e.userId === response)
.map((e) => (
<div key={e.userId.toString() + e.id.toString()}>
<li key={e.id}>{e.title}</li>
</div>
))}
<button
onClick={() => {
navigate('/');
}}
>
List of users
</button>
</div>
);
}
export default Posts;
I tried to use useEffect(), but it doesn't work in my case
<button
onClick={() => {useEffect(()=>{
navigate('/posts');
setResponse(e.id);},[])
}}
>
If you're navigating away to another page, you shouldn't be updating the state as you're navigating (which you're doing by calling setResponse after navigate).
To fix this error, you'd have to call navigate() after React finishes updating the response variable, which you can do by using a useEffect call at the top-level of your Home component:
// you should also pass the value of response to the Home component so it knows when it's been changed
function Home({ setResponse, response }) {
const [modal, setModal] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const state = useSelector((state) => state);
console.log(state);
useEffect(() => {
// This if statement checks if response === {}, which is a bit awkward; you should instead initialize your response state to a value like `null`
if (typeof response === 'object' && Object.keys(response).length === 0) {
return
}
navigate('/posts')
}, [response])
if (state.user.isLoading) {
return <h1>Loading...</h1>;
}
const toggleModal = () => {
setModal(!modal);
};
return (
<div className="App">
<button onClick={(e) => dispatch(fetchUsers())}>Fetch users</button>
{state.user.data &&
state.user.data.map((e) => (
<div key={e.id}>
<li key={e.name}>{e.name}</li>
<button
key={e.id + 10}
onClick={() => {
// navigate('/posts');
setResponse(e.id);
}}
className="btn"
>
Posts
</button>
<button onClick={toggleModal} key={e.id + 100} className="bnt">
Albums
</button>
</div>
))}
<Albums modal={modal} setModal={setModal} />
</div>
);
}
export default Home;
What I would like to be able to do is to initialize my context with a state and a function that updates that state.
For example, say I have the following:
export default function MyComponent () {
const MyContext = React.createContext()
const [myState, setMyState] = useState('1')
const contextValue = {
currentValue: myState,
setCurrentValue: (newValue) => setMyState(newValue)
}
return (
<MyContext.Provider value={contextValue}>
<MyContext.Consumer>
{e => <div onClick={() => e.setCurrentValue('2')}> Click me to change the value </div>}
{e.currentValue}
</MyContext.Consumer>
</MyContext.Provider>
)
}
The {e.currentValue} correctly outputs '1' at first, but when I click the button, nothing changes.
What I would expect is that e.setCurrentValue('2') would call setMyState('2'), which would update the state hook. This would then change the value of myState, changing the value of currentValue, and making '2' display.
What am I doing wrong?
You would want to return a fragment from the context as one JSX root.
Check here - https://playcode.io/931263/
import React, { createContext, useState } from "react";
export function App(props) {
const MyContext = React.createContext();
const [myState, setMyState] = useState("1");
const contextValue = {
currentValue: myState,
setCurrentValue: newValue => setMyState(newValue)
};
return (
<MyContext.Provider value={contextValue}>
<MyContext.Consumer>
{e => (
<>
<div onClick={() => e.setCurrentValue("2")}>
Click me to change the value
</div>
{e.currentValue}
</>
)}
</MyContext.Consumer>
</MyContext.Provider>
);
}
You're using e.currentValue outside of MyContext.Consumer context which does not have e, so it's throwing an error that e is not defined from e.currentValue.
You can wrap them up together under <MyContext.Consumer>{e => {}}</MyContext.Consumer>
function MyComponent() {
const MyContext = React.createContext();
const [myState, setMyState] = React.useState("1");
const contextValue = {
currentValue: myState,
setCurrentValue: (newValue) => setMyState(newValue),
};
return (
<MyContext.Provider value={contextValue}>
<MyContext.Consumer>
{(e) => (
<div>
<div onClick={() => e.setCurrentValue("2")}>
Click me to change the value
</div>
<div>{e.currentValue}</div>
</div>
)}
</MyContext.Consumer>
</MyContext.Provider>
);
}
ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I have 2 buttons. Add1 and Add2
The Add2 button in the Test does not work. What am I doing wrong. I'm not very familiar with React.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react-lite";
function App() {
const [component, setComponent] = useState([]);
useEffect(() => {});
const Test = observer(() => {
return (
<div>
<p>
Test -
<button onClick={() => setComponent([...component, Test])}>
Add 2
</button>
</p>
</div>
);
});
return (
<div>
{component.map((Input, index) => (
<Input key={index} />
))}
<button onClick={() => setComponent([...component, Test])}>Add 1</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
codesandbox: https://codesandbox.io/s/react-hooks-useeffect-forked-ml77pz
You should not create component inside another component if you do not keep its reference!
Move it outside of App and add prop setComponent
const Test = observer(({setComponent}) => {
return (
<div>
<p>
Test -
<button onClick={() => setComponent(component => [...component, Test])}>
Add 2
</button>
</p>
</div>
);
});
function App() {
...
}
Then when you render, pass 'setComponent' to it:
<Input key={index} setComponent={setComponent} />
You have not defined the base case for this recursive render
const Test = observer(() => {
return (
<div>
<p>
Test -
<button onClick={() => setComponent([...component, Test])}>
Add 2
</button>
</p>
</div>
);
});
I have two components, one which handles the buttons and another which is the index of the page.
The index of the page is structured as follows
const MainComp = () => {
const [text, setText] = useState(' ')
const test = [
'One',
'Two',
'Three',
]
return (
<>
<ChildComp onClick={() => test.map((i) => setText(i))} />
{text}
</>
)
}
In the button component I have this...
const ChildComp = ({ onClick }) => {
const test2 = [1,2,3]
return (
<>
<div>
{test2.map((data) => (
<button onClick={onClick}>
{data}
</button>
))}
</div>
</>
)
}
I want the value of each item in test to show up when the button is clicked.
Try this:
...
const test2 = [1, 2, 3]
const onClick = (item) => alert(item)
return (
<>
<div>
{test2.map((data) => (
<button onClick={() => onClick(data)}>{data}</button>
))}
</div>
</>
)
Try below code
const MainComp = () => {
const [text, setText] = useState(' ')
const test = [
'One',
'Two',
'Three',
]
return (
<>
{test.map(oneTest => <ChildComp data={oneTest} handleUpdateText={value => setText(value)}/>)}
</>
)
}
const ChildComp = ({ data, handleUpdateText }) => {
return (
<>
<button onClick={handleUpdateText}>
{data}
</button>
</>
)
}
Why don't you try to use the props the send the data from Main to the Child page..??You can do it as follows:
Main Page Code:
import React from 'react'
import Child from './Child'
const Main = ()=>{
const data = ["one","two","three"];
return(
<div>
<Child data={data}/>
</div>
)
}
export default Main;
Child Page code:
import React from 'react'
const Child =({data})=>{
const getData=()=>{
console.log(data)
}
return(
<div>
<button onClick={getData}>Transfer Data</button>
</div>
)
}
export default Child;
This way, when you click on the button the data from the Main page is transfered to the Child Page. Right now the data is showing in the console..but hey, you got the data from another page and now you can do whatever you want to do with that..!!
Not able to test button.simulate("click") on a Material-UI based Component having Button which fires 'loadAllData()' function on its onClick prop.
The below is my hooks based component
The full code for this component is here
const GithubMostPopularList = () => {
const globalStore = useSelector(state => state.globalStore)
const dispatch = useDispatch()
const loadAllData = () => {
const city = globalStore.city_to_search
setcurrentCityShown(city)
dispatch(loadMostPopularUsers(city, page, rowsPerPage))
}
return (
<div className={classes.container}>
<div className={classes.tableAndFabContainer}>
{globalStore.loading ? (
<div className={classes.spinner}>
<LoadingSpinner />
</div>
) : (
<div className={classes.table}>
<div className={classes.inputandButtonContainer}>
<Button
onClick={loadAllData}
variant="contained"
size="large"
color="primary"
disabled={globalStore.city_to_search === ""}
>
<Typography
variant="h3"
className={classes.modalButtonLabelEnabled}
>
Load City Data
</Typography>
</Button>
</div>
<div style={{ marginTop: "20px" }}>
<EachUserListItem
currentCityShown={currentCityShown}
></EachUserListItem>
</div>
</div>
)}
</div>
</div>
)
}
export default GithubMostPopularList
And below is my test, which fails giving me `TypeError: Cannot read property 'loadAllData' of null'
it("should trigger onClick on on Button press", () => {
const wrapperComp = mount(
<Provider store={store}>
<MuiThemeProvider theme={globalTheme}>
<GithubMostPopularList />
</MuiThemeProvider>
</Provider>,
)
const spy1 = jest.spyOn(wrapperComp.instance(), "loadAllData")
const button = wrapperComp.find(Button).last()
button.simulate("click")
wrapperComp.update()
expect(spy1).toHaveBeenCalledTimes(1)
})
Will highly appreciate any guidance or help.
I assume you had to use mount to avoid the error of shallowing a material component without a provider.
but here is what I usually do. I unwrap the component then it is ok to shallow it.
import unwrap from '#material-ui/core/test-utils/unwrap';
const UnwrappedComponent: any = unwrap((GithubMostPopularList as unknown as React.ReactElement<any>));
it('should trigger onClick on on Button press', () => {
const wrapperComp = shallow(<UnwrappedComponent />);
jest.spyOn(wrapperComp.instance(), 'loadAllData');
const button = wrapperComp.find(Button);
button.simulate('click');
wrapperComp.update();
expect(wrapperComp.instance().loadAllData).toHaveBeenCalledTimes(1);
});