I'm trying to use react Hooks to update my react component when a change to the state holding the api information occurs. I'm currently creating a quiz application where the user can create a quiz , by entering data such as title and answers, which is then sent to a mongodb. When the user is not editing the quiz, I want to display the updated values from the database .
For example:
parent component
import React,{useState} from 'react';
export default function Context() {
const [data,setData] = useState(null);
const [selected,setSelected] = useState(id); // id is taken from url
useEffect(() => {
getKahootQuestion();
}, [data])
async function getKahootQuestion(slide) {
if(selected !== undefined) {
let getKahootQuestion = await withData(`quizQuestion/single/${selected}`,'GET');
if(getKahootQuestion) {
setData(getKahootQuestion.data);
console.log(getKahootQuestion);
}
}
}
}
export default function MainContent() {
// I want to be able to add items to Database and re-render components to reflect the changes
let context = useContext(Context);
let title = context.data[0].title;
async function submitTitle(e) {
e.preventDefault();
let form = e.currentTarget;
let title = form['title'].value;
setEditable(false);
let updateKahootQuestion = await withData(`quizQuestion/${context.selected}`,'PUT',{title});
}
return (
<>
<form>
<input name='title' />
</form>
<p>{title}</p>
</>
)
}
I am not sure if your intention is to do this strictly through the context, but we will need more of it there to know what properties and functions are exposed from it.
Regardless, to trigger a re-render on your MainContent component you could also simply add in a local useState and update as needed. It should look something like this:
export default function MainContent() {
let context = useContext(Context);
// Instead of just declaring a title variable, declare a title state using that same information.
const [title, setTitle] = useState(context.data[0].title)
async function submitTitle(e) {
e.preventDefault();
let form = e.currentTarget;
let title = form['title'].value;
setEditable(false);
let updateKahootQuestion = await withData(`quizQuestion/${context.selected}`,'PUT',{title});
// Not sure what your updateKahootQuestion will look like, so for the sake of this example, will just use title.
setTitle(title) // will trigger a re-render
}
return (
<>
<form>
<input name='title' />
</form>
<p>{title}</p>
</>
)
}
Related
Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input
Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);
This is a next/react project.
folder structure:
components > Navbar.js
pages > index.js (/ route)(includes Navbar)
> submitCollection.js (/submitCollection)(includes Navbar)
I am trying to have the user submit a specific string as an input and i store it inside the account variable.
Navbar.js
const Navbar = ({}) => {
const [account,setAccount] = useState()
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
...
return (
<Link href="/">home</Link>
<Link href="/submitCollection">submit collection</Link>
...
<button onClick={handleClick} >press to set account</button>
...
{account?(<p>{account}</p>):(<p>u need to set an accout</p>)}
)
}
when i visit home using the navbar link, the account is again set to undefineed and i need to press the button again in order to set it. How can i make the string remain set. like persist on the navbar
useState is not persistent, it is bound to its component, in order to make it persist, you have to use localStorage
const [account,_setAccount] = useState();
const setAccount = (val) => {
_setAccount(val);
localStorage.setItem('account', val);
}
useEffect(() => {
const storedAccount = localStorage.getItem('account');
if (storedAccount) _setAccount(storedAccount);
}, [])
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
useEffect is called when the component renders, check for stored account and displays it.
And notice how we reimplement setAccount, so that everytime it is called, we update the localStorage.
You can also create a custom hook with this logic, so the component would look cleaner. Or even better, use something like use-state-persist
You can solve this problem using localstorage and useEffect
Adding this piece of code to your work will do the trick
const [account,setAccount] = useState(localStorage.getItem('account') ?localStorage.getItem('account') : null)
useEffect(()=>{
localstorage.setItem(account)
},[account])
For example
const [account,setAccount] = useState(localStorage.getItem('account') ?localStorage.getItem('account') : null)
useEffect(()=>{
localStorage.setItem('account',account)
},[account])
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
Hope it helped
I have a navbar component in which there is an input search bar. Currently I am taking the value of the search bar and navigate to the Results component and access the input value useParams.
I have the let [ result, setResult ] = useState([]); in my Results component because the results can change after the search is entered with buttons on the page. The problem is that I cannot set the initial result while defining the useState because I am fetching from an API.
So every time I render, I first get an empty array and failed promise, after which I get the desired one. How to fix this? I need the search bar to be in the navbar.
This is the code. New to React.
const Navbar = () => {
let navigate = useNavigate();
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
let value = event.target.value;
navigate(`/results/${value}`);
}
}
return (
<nav className='navigation'>
<div className='left-slot'>
<button>runtime</button>
</div>
<div className="middle-slot">
<input className="after"
placeholder="get runtimes" onKeyDown={handleKeyDown}>
</input>
</div>
<div className='right-slot'>
<button>How It Works</button>
<button>Coming Soon</button>
</div>
</nav>
);
}
const Results = () => {
let { value } = useParams();
let [ result, setResult ] = useState();
useEffect(() => {
fetchArrayByPage(value, page, option).then(res => setResult(res))
}, [value])
console.log(value);
console.log(result);
return (<div></div>)
}
I'm not entirely sure why your code does not work, so I'll provide three options.
Option 1 - If your problem is value is undefined.
Change your useEffect in Results to this:
useEffect(() => {
fetchArrayByPage(value && value.length ? value : '', page, option).then(res => setResult(res))
}, [value]);
Option 2 - If you need to pass props and Navbar and Results are not on separate routes.
Just pass value as props from Navbar to Results.
Option 3 - Passing components without props.
Use the Context API. This enables you to share data across components without needing to manually pass props down from parent to child.
Initialize variables in context.
Create separate file containing context.
import React, { createContext } from 'react';
const NavbarContext = createContext(null);
export default NavbarContext;
Import said context to App.js or App.tsx if you're using Typescript.
Declare variables and store them in an object for later reference.
const [value, setValue] = useState('');
...
const variables = {
value,
setValue,
...,
};
Wrap with Provider. Pass context variables to the Provider, enabling components to consume variables.
return (
<NavbarContext.Provider value={variables}>
...
</NavbarContext.Provider>
);
Import and use all your variables in Navbar and Results.
const { value, setValue, ... } = useContext(NavbarContext);
try a wrapping function for fetching and setting.
i would suggest something like this:
async function handleResults(){
const res = await fetchArrayByPage(value, page, option)
setResult(res)
}
then you can call it inside useEffect
Here is a parent component (Cart) and a child component (CartItem).
in the parent, there is a button that counts on the state that changes by the checkAvailability function which passed to the child via props,
import {useState} from "react";
import CartItem from "./CartItem";
const Cart = ({cart}) => {
const [available, setAvailable] = useState(true);
const checkAvailability = (check) => {
setAvailable(check)
}
return (
<>
{cart.items.map((item) => (
item.is_valid &&
<CartItem
key={item.id}
checkAvailability={checkAvailability}
/>
))}
<button disabled={available} >Click Me!</button>
</>
)
}
export default Cart;
in the child component, an API call returns true or false called by useEffect.
import {useState, useEffect} from "react";
const CartItem = ({checkAvailability}) => {
const [newData, setNewData] = useState(null);
const handleCheck = async () => {
const data = await api.call();
setNewData(data)
if(newData.available === false) {
checkAvailability(false)
} else if(newData.available === true) {
checkAvailability(true)
}
};
useEffect(() => {
handleCheck();
}, []);
return (
<div> Item </div>
)
};
export default CartItem;
issue:
every time the components mount, the API call in the child returns a value, that value gets passed to the parent by the checkAvailability function as a prop, which changes the state in the parent, when the state changes a re-render happen which restarts the circle infinitely.
the main thing is the button gets disabled when the API call returns a {false} value. if this way won't do the job, is there another way of doing it?.
what is the solution?.
Even if you resolved the current issue of infinite re-renders, you'll still have multiple API requests if there are multiple <CartItem/> components.
A better approach will be to move the API call to the parent. This will ensure it's called once regardless of the number of cart items it has. If the cart items need to know the value of available, then pass it to them.
const CartItem = ({ available }) => {
return <div> Item </div>
}
I am adding Cards dynamically in my React functional component. Cards are stored in State. I map them and give id to each of them. OnClick on those Cards I get their id successfully. Now I want to getElementById to change Card color:
function Clicked(pressedGifId) {
if (pressedGifId === 'correctGif') CorrectMatch();
else WrongMatch();
}
function CorrectMatch(pressedGifId) {
// / THERE I GET Element: null
console.log('Element:', document.getElementById(pressedGifId));
}
function WrongMatch() {
console.log('wrong a match!');
}
export default function GameObject(props) {
const addedToGameGif = [];
const [pressedGifId, gifPressed] = useState(null);
const [photoCards, setPhotoCards] = useState([]);
useEffect(() => {
Clicked(pressedGifId);
}, [pressedGifId]);
// add randomly picked photos to addedToGameGif array
// ...
addedToGameGif.map(gifId =>
photoCards.push(
<Card id={gifId} onClick={() => gifPressed(gifId)}>
text
</Card>,
),
);
return <div>{photoCards}</div>;
}
I tried learning refs but they are only for class components. So how do I reach my element by id in React?
You can use ref in functional component as well. There is a hook called useRef.
Note: Never interact directly with DOM until or unless there is no api available in react to solve the problem for that particular use case.
In react it's not recommended to interact directly with dom. Always use react apis to interact with dom. React is designed to hide the DOM because they want to abstract the DOM away. By using the DOM directly you break the abstraction and make your code brittle to changes introduced in the library.
React is maintaining a virtual DOM if we make any changes in actual DOM directly then react will not be aware of this change and this can lead to some unexpected behavior .
import React, {useState, useRef} from 'react';
export default function GameObject(props) {
const addedToGameGif = [];
const [pressedGifId, gifPressed] = useState(null);
const [photoCards, setPhotoCards] = useState([]);
const elemRef = useRef(null);
useEffect(() => {
Clicked(pressedGifId);
}, [pressedGifId]);
// add randomly picked photos to addedToGameGif array
// ...
addedToGameGif.map(gifId =>
photoCards.push(
<Card ref={elemRef} id={gifId} onClick={() => gifPressed(gifId)}>
text
</Card>
)
);
return <div>{photoCards}</div>;
}
Example from official docs.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}