Component don't re render after state update - reactjs

I'm testing out react-sweet-state as an alternative to Redux, but I feel like I'm missing something.
This is how my store is written :
import { createStore, createHook } from 'react-sweet-state'
const initialState = {
startHolidayButton: true,
}
const actions = {
setStartHolidayButton: value => ({ setState }) => {
setState({ startHolidayButton: value })
},
}
const ButtonsVisibleStore = createStore({
initialState,
actions,
name: 'ButtonsVisibleStore',
})
export const useButtonsVisible = createHook(ButtonsVisibleStore)
In my eyes it all seems pretty fine, the hooks works as long as I only need the initial state.
This is how I access and modify it :
const App = () => {
const [state, actions] = useButtonsVisible()
return (
<Styled>
<MainMenu />
<div className="l-opacity">
<WorldMap />
</div>
{state.startHolidayButton && (
<div
className="bottom-toolbar"
onClick={() => {
actions.setStartHolidayButton(false)
}}>
<StartSelectionButton />
</div>
)}
</Styled>
)
}
I can read the values from the state and I can trigger the actions but if an action updates a state value, my component don't re render, it's like he is unaware that something has been updated.
So am I doing it the right way or are actions not meant for this?

export default function App() {
const [state, actions] = useButtonsVisible();
return (
<div className="App">
{state.startHolidayButton ?
(
<div
className="bottom-toolbar"
onClick={() => {
actions.setStartHolidayButton(false);
}}
>
click me
</div>
)
:
(
<div
className="bottom-toolbar green"
onClick={() => {
actions.setStartHolidayButton(true);
}}
>
click me
</div>
)
}
</div>
);
}
With some changes your code works fine. Here is the css: .bottom-toolbar { display: inline-block; width: 100%; height: 50px; background: red; cursor: pointer; } .bottom-toolbar.green { background: green; }
https://codesandbox.io/s/elegant-mendeleev-o9zv7?file=/src/styles.css:58-218

Related

Why am I receiving this error "Uncaught TypeError: state.map is not a function" it only happens when I set my setState in my handleClick function

It's saying state.map is not a function but it works just fine before i call the handleclick function.I am stuck here and would like to know why its giving me the state.map is not a function error. Added bonus too would
be me asking how does one setState when you have objects inside objects.
For example if id like to return with setStatea {...item, value:false} value is inside and object thats inside and object. How would I specifically target the value I need inside and array of objects. Thank you.
import React from "react";
import Quiz from "./components/Quiz";
import Menu from "./components/Menu";
import { v4 as uuidv4 } from 'uuid'
function App() {
const [state,setState] = React.useState([])
const [start,setStart] = React.useState(false)
React.useEffect(()=> {
if(start === true){
console.log(state)
fetch("https://opentdb.com/api.php?amount=5&category=27&difficulty=easy&type=multiple")
.then(res => res.json())
.then(data => setState(data.results.map(item =>
({selectedQuestion: "",
buttons: item.incorrect_answers.concat(item.correct_answer).map(item =>
({use: item ,value: false, id: uuidv4()})),
questions: item.question
}))))
}} , [start])
function startGame () {
setStart(prevState => !prevState)
}
const myButtons = state.map(function(item) {
return (<Quiz
key={uuidv4()}
buttons={item.buttons}
value ={item.buttons.map(item => item.value)}
question = {item.questions}
handleClick ={(event) =>handleClick(event.target.value)}
/> ) })
console.log(state.map(item => item))
function handleClick (event) {
if (event === "false"){
//this is what is giving me issues//
setState(item => (
{
...item, selectedQuestion: "howdy"
}))
//////////////////////////////////////////////////////////////
}
else {
console.log("hi")
}
}
return (<div>
{start === false ? <Menu
startGame ={startGame}
/> : <div className="parent--quiz">
<div className="quiz">
{myButtons}
</div>
</div>
}
</div>
)
}
export default App;
Quiz component is here
function Quiz (props) {
const styles ={
backgroundColor: props.value === true ? "red" : "#D6DBF5"
}
return (
<div>
<div className="cards" style={{ borderTop: "2px solid #fff ", marginLeft: 20, marginRight: 20 }}>
<h1 className="quiz--h1">{props.question}</h1>
<div className="quiz--buttons-div" >
{props.buttons.map((item) => (
<button className="quiz--buttons" style={styles} value={item.value} onClick={props.handleClick}>{item.use}</button>
))}
</div>
</div>
</div>
)
}
export default Quiz
code sections are fine

How save style in the local storage

I have such a project. Here I want the button border save in the local storage.The buttons are divided into categories. For example when you refresh the page after selecting a sports button, the border of the button disappears. I want save btn border in the localstorage. I saved the categories in memory, but I can't make the border of the selected button.How can I fix it?
import React, { useEffect, useState } from "react";
import SpinnerLoad from './components/SpinnerLoad'
import NewsItem from "./components/NewsItem";
import Category from "./components/data/Category"
const App = () => {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false)
const [selected, setSelected] = useState('');
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
setLoading(false);
};
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
<button onClick={() =>{ fetchValue(category,i) ; setSelected(i)} }
style={{border : selected === i ? '1px solid red' : null}} >{category}</button>
);
useEffect(() => {
let categoryValue = localStorage.getItem("category") || "all";
fetchValue(categoryValue)
const select = localStorage.getItem("selected") || "";
setSelected(select);
}, []);
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value,i) => {
return <CategoryButton category={value} i={i}/>;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad />
:
state.map((data, index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
key={data.id}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default App;
According to the code looks like you want to display data specific to a category set when the user clicks on the category buttons. and after the click, the correct data is rendered and the current category button receives a change in its style highlighting it is the current state.
I don't understand why you need to store anything in a client's localstorage,
I would not recommend storing too much in localStorage as it is limited and is used by different sites a user visits, I only store authentication tokens in localstorage and I believe that is the norm.
I've tried to create the effect you want without the need to store in local storage
import React, { useState, useCallback, useEffect } from "react";
import ReactDOM from "react-dom";
import { cat } from "../categories.js";
import { news } from "../news.js";
function Example() {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState(null);
useEffect(() => {
function fetchFunction() {
setLoading(true);
for (let i = 0; i < news.length; i++) {
if (news[i].id === selected) {
const current = news[i].c;
setState(current);
}
}
setLoading(false);
}
fetchFunction();
}, [selected]);
return (
<>
<ol
style={{
width: "50%",
listStyle: "none",
display: "flex",
justifyContent: "space-between"
}}
>
{cat.map((item, index) => {
return (
<li key={index}>
<button
style={{ border: selected === item.id && "none" }}
onClick={() => {
setSelected(item.id);
}}
>
{item.name}
</button>
</li>
);
})}
</ol>
<section style={{ width: "100%", height: "70%" }}>
{state.map((item, index) => {
return (
<div
key={index}
style={{
width: "30%",
height: "30%",
background: "red",
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "1% 0 2% 0"
}}
>
{item.name}
</div>
);
})}
</section>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
You can save the selectedIndex in localStorage and retrieve it in the useEffect..
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
// setting selected as string instead of index for type checking
<button onClick={() =>{ fetchValue(category,i) ; setSelected(`${i}`)} }
style={{border : selected === `${i}` ? '1px solid red' : null}} >{category}</button>
);
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
// ...
}
useEffect(() => {
const select = localStorage.getItem("selected") || "";
// passing selectedIndex to the fetchValue, otherwise it becomes
//undefined..
fetchValue(categoryValue,select)
setSelected(select);
},[])

How can I render an icon dynamically?

How can I render only the icon cartIcon dynamically? Because right now, like the code below, when I enter in the component with the mouse, all the icons appears not only the icon of the single product.
I think because of map but how can I render only to it?
interface IItemsProps {
products: ProductsType;
}
const Items: React.FunctionComponent<IItemsProps> = ({ products }) => {
const [state, setState] = React.useState<boolean>(false);
const handleMouseEnter = () => {
setState(true);
};
const handleMouseLeave = () => {
setState(false);
};
const itemUI = products.map((item: SingleProductsType) => {
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state && <IconsCarts />} ** //HERE I NEED TO SHOW THIS COMPONENT ONLY WHEN I
// ENTER WITH THE MOUSE BUT ONLY FOR THE SELECTED
//PRODUCT NOT ALL OF THEM **
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
});
return <WrapperItems>{itemUI}</WrapperItems>;
};
export default Items;
You could store the hovered _id in state, so you know which one it was.
const [state, setState] = React.useState<string | null>(null); // or `number` ?
Then
{state === _id && <IconsCarts />}
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={() => setState(_id)}
onMouseLeave={() => setState(null)}
/>
Or you could move the useState into a component that is called every loop of your map, so that each item has its own private state.
function MyItem({item}: { item: SingleProductsType }) {
const [state, setState] = React.useState<boolean>(false);
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state && <IconsCarts />}
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
}
Now you can do:
{products.map((item: SingleProductsType) => <MyItem item={item} />}
Lastly, if all you want to do is show/hide the cart icon when you enter some element with the mouse, this solution is probably way overkill. You can do this with CSS alone, which is going to be a far cleaner solution since it takes no javascript code whatsoever, and you don't have to track state at all.
.item {
width: 100px;
height: 100px;
background: #aaa;
margin: 10px;
}
.item button {
display: none;
}
.item:hover button {
display: block;
}
<div class="item">
Foo
<button>Add to cart</button>
</div>
<div class="item">
Bar
<button>Add to cart</button>
</div>
<div class="item">
Baz
<button>Add to cart</button>
</div>
With a boolean in state, all you know is whether to show an icon, but what about knowing which list item to show the icon on? Instead of state being a boolean, how about we use the index of the product.
interface IItemsProps {
products: ProductsType;
}
const Items: React.FunctionComponent<IItemsProps> = ({ products }) => {
const [state, setState] = React.useState<number>(-1);
const handleMouseEnter = (index) => {
setState(index);
};
const handleMouseLeave = () => {
setState(-1);
};
const itemUI = products.map((item: SingleProductsType, index: number) => {
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state === index && <IconsCarts />} ** //Check if index matches state before showing icon **
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
});
return <WrapperItems>{itemUI}</WrapperItems>;
};
export default Items;
Now the condition to show the icon is if the index of the list item matches the index in state. And we pass in the index to handleMouseEnter to set state to that index, and handleMouseLeave will reset it back to -1.

How to stop child components resetting on parent setState

I have been searching for a while but can't seem to find a concrete answer to this problem. I have encountered this many times and overcame them by using "hacky" solutions. What is the recommended/standard way to get around child components re-rendering when setting parent state.
I have made a very simple example to show this problem where we have a left side child component with a count state and a right side child component which displays text when a button on the left side is clicked.
Example.js
import React, { useState } from "react";
import "../css/pages/Example.css";
function Example() {
const [rightText, setRightText] = useState();
const LeftSide = () => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
};
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
return (
<div className="wrapper">
<LeftSide />
<RightSide />
</div>
);
}
export default Example;
Example.css
.wrapper {
width: 300px;
height: 300px;
border: 1px solid red;
display: flex;
}
.egleft {
border: 1px solid green;
width: 50%;
}
.egleft p,
.egright p {
color: black;
}
.egright {
width: 50%;
border: 1px solid blue;
}
To replicate: click add button to increment value on left side.. then click update right. You will see due to the parent's setState, left side is re-rendered, causing its own count state to be reset to the initial value.
Here is a gif showing the problem
Thanks
The issue is you are creating the LeftSide (and it's state) and RightSide components on the fly each time the Example component re-renders.
It's not common practice to create child components inside the render function of the parent as it creates an unecessary overhead (creating a new function every render instead of using the same existing function). You can split up the components into multiple functions in the root of the file, or multiple files, and compose them like #dejan-sandic answer.
But if creating the child component inside the render function is a must. You can use the useMemo hook to stop react from recreating your child component:
import React, { useState, useMemo } from "react";
import "../css/pages/Example.css";
function Example() {
const [rightText, setRightText] = useState();
const LeftSide = useMemo(() => () => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
}, []);
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
return (
<div className="wrapper">
<LeftSide />
<RightSide />
</div>
);
}
export default Example;
But I would advise against it, as it can become unnecessarily complex, unless for something extremely dynamic.
You are defining both RightSide and the LeftSide components inside of Example. You can't do that because those two components will be created every time` Example component renders, which is happening after every statechange.
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
Do this in stead:
const LeftSide = ({ setRightText }) => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
};
const RightSide = ({ rightText }) => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
function Example() {
const [rightText, setRightText] = useState();
return (
<div className="wrapper">
<LeftSide rightText={rightText} />
<RightSide setRightText={setRightText} />
</div>
);
}

How to prevent Link from messing up my radio buttons behavior?

I am trying to build a menu, that shows the current page in bold.
For that I am using radio buttons so that each item on the menu is a label for that radio button, and in my css I make an active item bold.
My problem is, that because the label is wrapped in a Link element, when an item is clicked nothing really changes. It navigates properly but the radio button sate doesn't change. Maybe everything just re renders ignoring my action?
It works just fine without the link element. Why is that? And what can I do to make it work?
This is the code for my menu component:
import "./styles.scss";
import React from "react";
import { Link } from "react-router-dom";
const Menu = () => {
const turnToLowerCaseWithHyphen = string => {
return (string[0].toLowerCase() + string.slice(1)).replace(" ", "-");
};
const renderMenuItems = array => {
return array.map(item => {
const smallHyphenedItem = turnToLowerCaseWithHyphen(item);
return (
<div className="flex-group" key={smallHyphenedItem}>
<input
className="menu-item__radio"
id={smallHyphenedItem}
type="radio"
name="menu-items"
onChange={() => console.log(smallHyphenedItem)}
/>
<Link
to={"/" + smallHyphenedItem}
className="menu-item"
key={smallHyphenedItem}
>
<label htmlFor={smallHyphenedItem} className="menu-item__label">
{item}
</label>
</Link>
</div>
);
});
};
return (
<div className="menu">
{renderMenuItems(["Feed", "Search", "Contact us"])}
</div>
);
};
export default Menu;
EDIT: I've tried to use a state in the menu component but that doens't help either:
const Menu = () => {
const [currentPage, setCurrentPage] = useState(null);
const turnToLowerCaseWithHyphen = string => {
return (string[0].toLowerCase() + string.slice(1)).replace(" ", "-");
};
const renderMenuItems = array => {
return array.map(item => {
const smallHyphenedItem = turnToLowerCaseWithHyphen(item);
return (
<div className="flex-group" key={smallHyphenedItem}>
<input
className="menu-item__radio"
id={smallHyphenedItem}
type="radio"
name="menu-items"
checked={currentPage === smallHyphenedItem}
onChange={() => setCurrentPage(smallHyphenedItem)}
/>
<label htmlFor={smallHyphenedItem} className="menu-item__label">
<Link
to={"/" + smallHyphenedItem}
className="menu-item"
key={smallHyphenedItem}
>
{item}
</Link>
</label>
</div>
);
});
};
return (
<div className="menu">
{renderMenuItems(["Feed", "Search", "Contact us"])}
</div>
);
};
export default Menu;
This was a bit more of a headach than I've expected, but I got it working.
Like Sasha suggested, I needed to store the choice in a state using redux to have it persist between pages.
But that isn't enough. using Link didn't allow for the action to be executed before navigating (to my understanding).
What I had to do was instead of using Link, to just navigate to the page I wanted using the history.push() command.
This is my final working code:
import "./styles.scss";
import React from "react";
import { connect } from "react-redux";
import { Link, useHistory } from "react-router-dom";
import history from "../../../history";
import { setCurrentPage } from "../../../actions";
const Menu = ({ setCurrentPage, page }) => {
const myHistory = useHistory(history);
console.log(page);
const turnToLowerCaseWithHyphen = string => {
return (string[0].toLowerCase() + string.slice(1)).replace(" ", "-");
};
const handleChange = (page) => {
setCurrentPage(page);
myHistory.push(`/${page}`)
};
const renderMenuItems = array => {
return array.map(item => {
const smallHyphenedItem = turnToLowerCaseWithHyphen(item);
return (
<div className="flex-group" key={smallHyphenedItem}>
<input
className="menu-item__radio"
id={smallHyphenedItem}
type="radio"
name="menu-items"
checked={page === smallHyphenedItem}
onChange={() => handleChange(smallHyphenedItem)}
/>
<label htmlFor={smallHyphenedItem} className="menu-item__label">
{/* <Link
to={"/" + smallHyphenedItem}
className="menu-item"
key={smallHyphenedItem}
> */}
{item}
{/* </Link> */}
</label>
</div>
);
});
};
return (
<div className="menu">
{renderMenuItems(["Feed", "Search", "Contact us"])}
</div>
);
};
const mapStateToProps = state => {
return {
page: state.page
};
};
export default connect(mapStateToProps, { setCurrentPage })(Menu);
And this is my CSS:
.menu {
display: grid;
grid-template-columns: repeat(4, max-content);
grid-gap: 3rem;
&-item {
&__label {
font-family: var(--font-main);
font-size: 1.6rem;
transition: all 0.2s;
text-decoration: none;
color: var(--color-grey-medium);
width: max-content;
&:hover {
color: var(--color-main);
}
}
&__radio {
visibility: hidden;
opacity: 0;
&:checked + .menu-item__label {
color: var(--color-main);
}
}
}
}

Resources