Very simple react destructuring issue with a custom hook - reactjs

I'm amazed that I can't solve this myself, but I'm having a basic issue here. Essentially I just want to destructure the user variable in the useSetWelcome hook to prevent the use of verbose chaining such as user.user.email - for instance, const { email } = user does not work and instead needs user.user.
I tried changing from const useSetWelcome = user => { to const useSetWelcome = ({ user }) => {, but that results in an infinite loop.
Where am I going wrong here? My code demo: https://stackblitz.com/edit/react-b1jroe
And the code:
import React, { useState, useEffect } from 'react';
const joe = {
name: 'Joe',
email: 'joe#bloggs.com'
};
// const useSetWelcome = ({ user }) => { // infinite loop problem
const useSetWelcome = user => {
const [name, setName] = useState(null);
const [welcomeMsg, setWelcomeMsg] = useState('No user detected');
// const { email } = user; // needs user.user
// console.log('user', user);
// console.log('{user}', { user });
// console.log('user.email', user.email); // should be joe#bloggs.com
// console.log('email', email); // should be joe#bloggs.com
console.log('user?.user?.email', user?.user?.email); // works
if (user.name) {
setName(user.name);
setWelcomeMsg('welcome ' + user.name);
}
return { name, welcomeMsg };
};
const App = () => {
// const [user, setUser] = useState(joe); // joe or {joe}? and why?
const [user, setUser] = useState(joe);
console.log('state user', user);
const toggleLogin = user => {
if (user) {
setUser(null);
} else {
setUser(joe);
}
};
const loginMsg = user ? 'Logout' : 'Login';
const Welcome = user => {
const { name, welcomeMsg } = useSetWelcome(user);
return (
<p>
{welcomeMsg} {name}
</p>
);
};
return (
<div>
<Welcome user={user} />
<button onClick={() => toggleLogin(user)}>{loginMsg}</button>
</div>
);
};
export default App;

The problem is, <Welcome /> is a component. A component receives only one parameter, props. So, when you write this: const Welcome = user => {, its actually const Welcome = props => {.
Long story short, change this line to const Welcome = ({ user }) => { (so you destruct user from props) and it will work.
P.S.: You're getting an infinite loop because inside your useSetWelcome hook, you have this condition:
if (user.name) {
setName(user.name)
}
When you use setName, the entire hook rerenders, and the condition is tested again. Again, user.name will exist, and setName will get called again, and again, and forever. To achieve what I think you intended to, you have to improve the condition to something like this:
if (user.name && user.name !== name) {
setName(user.name);
setWelcomeMsg('welcome ' + user.name);
}

Related

How can I make syntax less repetitive (DRY)?

The object of this app is to allow input text and URLs to be saved to localStorage. It is working properly, however, there is a lot of repeat code.
For example, localStoredValues and URLStoredVAlues both getItem from localStorage. localStoredValues gets previous input values from localStorage whereas URLStoredVAlues gets previous URLs from localStorage.
updateLocalArray and updateURLArray use spread operator to iterate of previous values and store new values.
I would like to make the code more "DRY" and wanted suggestions.
/*global chrome*/
import {useState} from 'react';
import List from './components/List'
import { SaveBtn, DeleteBtn, DisplayBtn, TabBtn} from "./components/Buttons"
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
//these items are used for the state of localStorage
const [display, setDisplay] = useState(false);
const localStoredValues = JSON.parse(
localStorage.getItem("localValue") || "[]"
)
let updateLocalArray = [...localStoredValues, leadValue.inputVal]
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = JSON.parse(localStorage.getItem("URLValue") || "[]")
const tabBtn = () => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const url = tabs[0].url;
setMyLeads((prev) => [...prev, url]);
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
localStorage.setItem("URLValue", JSON.stringify(updateURLArray));
});
setDisplay(false)
};
//handles change of input value
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
setDisplay(false);
// update state of localStorage
localStorage.setItem("localValue", JSON.stringify(updateLocalArray))
};
const displayBtn = () => {
setDisplay(true);
};
const deleteBtn = () => {
window.localStorage.clear();
setMyLeads([]);
};
const listItem = myLeads.map((led) => {
return <List key={led} val={led} />;
});
//interates through localStorage items returns each as undordered list item
const displayLocalItems = localStoredValues.map((item) => {
return <List key={item} val={item} />;
});
const displayTabUrls = URLStoredVAlues.map((url) => {
return <List key={url} val={url} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<TabBtn tabBtn={tabBtn} />
<DisplayBtn displayBtn={displayBtn} />
<DeleteBtn deleteBtn={deleteBtn} />
<ul>{listItem}</ul>
{/* displays === true show localstorage items in unordered list
else hide localstorage items */}
{display && (
<ul>
{displayLocalItems}
{displayTabUrls}
</ul>
)}
</main>
);
}
export default App
Those keys could be declared as const and reused, instead of passing strings around:
const LOCAL_VALUE = "localValue";
const URL_VALUE = "URLValue";
You could create a utility function that retrieves from local storage, returns the default array if missing, and parses the JSON:
function getLocalValue(key) {
return JSON.parse(localStorage.getItem(key) || "[]")
};
And then would use it instead of repeating the logic when retrieving "localValue" and "URLValue":
const localStoredValues = getLocalValue(LOCAL_VALUE)
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = getLocalValue(URL_VALUE)
Similarly, with the setter logic:
function setLocalValue(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}
and then use it:
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
setLocalValue(URL_VALUE, updateURLArray);
// update state of localStorage
setLocalValue(LOCAL_VALUE, updateLocalArray)

First time input doesn't get submitted in React

I'd like make an API call, which user input makes part of the API URL. Data is only fetched on demand after user submit.
My problem is: after first time input and submit, input is processed as an empty string, constructed wrong URL and made API call. (still loads data but the wrong data)
Only after second submit does it get actual user input, construct the correct URL and display the right data.
monitering network:
User input is stored in enteredWallet, Console.log(enteredWallet) prints the input, but setOwner(enteredWallet) doesn't change owner to be enteredWallet.
import { useState } from "react";
// example input: 0x147412d494731cbb91dbb5d7019464a536de04dc
function App() {
const [data, setData] = useState([]);
const [enteredWallet, setEnteredWallet] = useState("");
const [owner, setOwner] = useState("");
const walletChangeHandler = (event) => {
setEnteredWallet(event.target.value);
};
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
fetchNFTHandler();
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
};
function fetchNFTHandler() {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${owner}&order_direction=desc&offset=0&limit=10`
)
.then((res) => {
return res.json();
})
.then((data) => {
const transformedData = data.assets.map((element, index) => {
return {
title: element.name,
id: index,
};
});
setData(transformedData);
console.log("fetched");
});
}
return (
<div className="App">
<header className="App-header">
<h3>Show me assets in this wallet</h3>
<form onSubmit={submittedHandler}>
<input
placeholder="wallet address"
value={enteredWallet}
onChange={walletChangeHandler}
/>
<button>Submit</button>
</form>
<div>
{data.map((element) => (
<li key={element.id}>{element.title}</li>
))}
</div>
</header>
</div>
);
}
export default App;
Because owner in fetchNFTHandler doesn't update immediately after call setOwner.
Why don't use onwer as a param.
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
fetchNFTHandler(enteredWallet); //here
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
};
function fetchNFTHandler(owner) {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${owner}&order_direction=desc&offset=0&limit=10`
)...
or if you need use it as state indeed.
use useEffect to call fetchNFTHandler
useEffect(() => {
fetchNFTHandler();
}, [owner]) // when owner change, fetchNFTHandler will be call
If you want use a variable, it can take effect at once. you can try useRef.
const ownerRef = useRef("");
const submittedHandler = (event) => {
event.preventDefault();
ownerRef.current = enteredWallet;
fetchNFTHandler();
};
function fetchNFTHandler(owner) {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${ownerRef.current}&order_direction=desc&offset=0&limit=10`
)...
The function returned by useState (in your case, setEnteredWallet or setOwner) is not synchronous. The state is not immediately changed after calling either it. If you want to call fetchNFTHandler every time enteredWallet changes, you can use useEffect. Or simply, you can pass enteredWallet to fetchNFTHandler as a parameter. An example usage of useEffect:
useEffect(() => {
fetchNFTHandler();
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
}, [owner, enteredWallet]) // Call method above when owner or enteredWallet change
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
// You don't need the following lines anymore
// fetchNFTHandler();
// console.log("enteredWallet:", enteredWallet);
// console.log("owner:", owner);
};

React hooks with useState

I've got the following code:
export default function App() {
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type) => {
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body,
};
setLastMessageId(newMessage.id)
setMessages([...messages, newMessage]);
console.log("point 1", messages);
return newMessage.id;
}
// remove a message with id
const removeMessage = (id) => {
const filter = messages.filter(m => m.id !== id);
console.log("point 2", filter);
setMessages(filter);
}
// add a new message and then remove it after some seconds
const addMessageWithTimer = (body, type="is-primary", seconds=5) => {
const id = addMessage(body, type);
setTimeout(() => removeMessage(id), seconds*1000);
};
return (
...
);
}
I would like to know why after I setMessages at point 1, when I do console log it doesn't appear to be updated. This turns into a weird behaviour when I call addMessageWithTimer because when it calls removeMessage then it doesn't remove correctly the messages that I expect.
Could you please explain me how to do it?
Just like setState in class-components, the update functions of useState don't immediately update state, they schedule state to be updated.
When you call setMessages it causes react to schedule a new render of App which will execute the App function again, and useState will return the new value of messages.
And if you think about it from a pure JS perspective, messages can't change: it's just a local variable, (a const one, even). Calling a non-local function can't cause a local variable's value to change, JS just doesn't work that way.
#Retsam is correct in his explanation.
I think you would get an issue if you don't use setTimeout in addMessageWithTimer. Isn't it? But for now, it is correct.
If you don't want to give a timer of 5 seconds and still want to keep it running correctly, then give a timer of 0 seconds. It would still work okay.
what weird behavior your seeing?
when I tried your code, I'm able to remove the added message after 5 sec.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
let bodyText = "";
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type) => {
if (body === "") return;
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body
};
setLastMessageId(newMessage.id);
setMessages([...messages, newMessage]);
bodyText = "";
return newMessage.id;
};
// remove a message with id
const removeMessage = (id) => {
const filter = messages.filter((m) => m.id !== id);
console.log("point 2", filter);
setMessages(filter);
};
// add a new message and then remove it after some seconds
const addMessageWithTimer = (body, type = "is-primary", seconds = 5) => {
const id = addMessage(body, type);
setTimeout(() => removeMessage(id), seconds * 1000);
};
console.log("point 1", messages);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<input onChange={(e) => (bodyText = e.target.value)} />
<button onClick={(e) => addMessage(bodyText, "is-primary")}>
Add messsage
</button>
<button onClick={(e) => addMessageWithTimer(bodyText, "is-primary", 5)}>
Add temp messsage
</button>
{messages.map((message, id) => {
return (
<div key={id}>
<p>
{message.id} {message.body}
</p>
</div>
);
})}
</div>
);
}
#Retsam was very useful with his answer as I was able to understand the problem and find a proper solution.
here is the solution that I've found:
export default function App() {
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type="is-primary") => {
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body
};
setLastMessageId(newMessage.id)
setMessages([...messages, newMessage]);
return newMessage.id;
}
// delete messages after 5 seconds
useEffect(() => {
if (!messages.length) return;
const timer = setTimeout(() => {
const remainingMessages = [...messages];
remainingMessages.shift();
setMessages(remainingMessages);
}, 5*1000);
return () => clearTimeout(timer);
}, [messages]);
return (
...
);
}

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

Set value to textfield with hooks and redux material ui

I'm building an app using react, redux, and redux-saga.
The situation is that I'm getting information from an API. In this case, I'm getting the information about a movie, and I will update this information using a basic form.
What I would like to have in my text fields is the value from the object of the movie that I'm calling form the DB.
This is a brief part of my code:
Im using 'name' as an example.
Parent component:
const MovieForm = (props) => {
const {
movie,
} = props;
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
name,
});
};
const handleSetValues = () => {
console.log('hi');
console.log(movie, name);
setName(movie.name);
setValues(true);
};
useEffect(() => {
if (movie && values === false) {
handleSetValues();
}
});
return (
<Container>
<TextField
required
**defaultValue={() => {
console.log(movie, name);
return movie ? movie.name : name;
}}**
label='Movie Title'
onChange={(e) => setName(e.target.value)}
/>
</Container>
);
};
export default MovieForm;
....
child component
const MovieUpdate = (props) => {
const { history } = props;
const { id } = props.match.params;
const dispatch = useDispatch();
const loading = useSelector((state) => _.get(state, 'MovieUpdate.loading'));
const created = useSelector((state) => _.get(state, 'MovieUpdate.created'));
const loadingFetch = useSelector((state) =>
_.get(state, 'MovieById.loading')
);
const movie = useSelector((state) => _.get(state, 'MovieById.results'));
useEffect(() => {
if (loading === false && created === true) {
dispatch({
type: MOVIE_UPDATE_RESET,
});
}
if (loadingFetch === false && movie === null) {
dispatch({
type: MOVIE_GET_BY_ID_STARTED,
payload: id,
});
}
});
const updateMovie = (_movie) => {
const _id = id;
const obj = {
id: _id,
name: _movie.name,
}
console.log(obj);
dispatch({
type: MOVIE_UPDATE_STARTED,
payload: obj,
});
};
return (
<div>
<MovieForm
title='Update a movie'
buttonTitle='update'
movie={movie}
onCancel={() => history.push('/app/movies/list')}
onSubmit={updateMovie}
/>
</div>
);
};
export default MovieUpdate;
Then, the actual problem is that when I use the default prop on the text field the information appears without any problem, but if i use defaultValue it is empty.
Ok, I kind of got the answer, I read somewhere that the defaultValue can't be used int the rendering.
So I cheat in a way, I set the properties multiline and row={1} (according material-ui documentation) and I was able to edit this field an receive a value to display it in the textfield

Resources