How to update state conditionally in Stateless Component? - reactjs

I have two components. MyButton and MyLabel component. I created 3 MyButton Compoents and 3 MyLabel Components. Each button has a different increment value. When you click on a button , the respective label should be updated not all the labels. At present all the labels are updating.
function MyButton(props) {
const onclick = () => {
props.onNumberIncrement(props.toBeIncremented);
};
return <button onClick={onclick}>+{props.toBeIncremented}</button>;
}
const MyLabel = function(props) {
return <label> {props.counter}</label>;
};
function App(props) {
const [counter, mySetCounter] = React.useState(0);
const handleClick = (incrementValue) => {
mySetCounter(counter + incrementValue);
};
return (
<div>
<MyButton
counter={counter}
onNumberIncrement={handleClick}
toBeIncremented={5}
/>
<MyButton
counter={counter}
onNumberIncrement={handleClick}
toBeIncremented={10}
/>
<MyButton
counter={counter}
onNumberIncrement={handleClick}
toBeIncremented={15}
/>
<br />
<MyLabel counter={counter} />
<MyLabel counter={counter} />
<MyLabel counter={counter} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
jsfiddle:
click here

Create a generator of button/label pairs with their local state and step. Generate the buttons and labels, and render them:
const useGenerateButtonAndLabel = step => {
const [counter, mySetCounter] = React.useState(0);
const onclick = React.useCallback(
() => mySetCounter(counter + step),
[step, counter]
);
return [
<button onClick={onclick}>+{step}</button>,
<label> {counter}</label>
];
};
function App(props) {
const [button1, label1] = useGenerateButtonAndLabel(5);
const [button2, label2] = useGenerateButtonAndLabel(10);
const [button3, label3] = useGenerateButtonAndLabel(15);
return (
<div>
{button1}
{button2}
{button3}
<br />
{label1}
{label2}
{label3}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('demo'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="demo"></div>
If you also need a total, each generated pair can also return it's current counter, and you can sum them in the parent. In this example, I also automate the items creation/rendering with Array.from(), map, and reduce.
const useGenerateButtonAndLabel = step => {
const [counter, mySetCounter] = React.useState(0);
const onclick = React.useCallback(
() => mySetCounter(counter + step),
[step, counter]
);
// step is used here is a key, but if step is not unique, it will fail. You might want to generate a UUID here
return [
<button key={step} onClick={onclick}>+{step}</button>,
<label key={step}> {counter}</label>,
counter
];
};
const sum = items => items.reduce((r, [,, counter]) => r + counter, 0);
function App(props) {
const items = Array.from({ length: 5 },
(_, i) => useGenerateButtonAndLabel(5 * (i + 1))
);
return (
<div>
{items.map(([button]) => button)}
<br />
{items.map(([, label]) => label)}
<div>Total: {sum(items)}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('demo'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="demo"></div>

Here is another solution using a single state variable :
function App(props) {
const [appState, setAppState] = React.useState([
{value: 0, incrementValue: 5},
{value: 0, incrementValue: 10},
{value: 0, incrementValue: 15}
]);
const handleClick = clickedIndex => () => {
setAppState(
appState.map((item, index) => clickedIndex !== index ?
item : ({...item, value: item.value + item.incrementValue})
)
);
};
return (
<div>
{
appState.map(({value, incrementValue}, index) => (
<MyButton
key={index}
counter={value}
onNumberIncrement={handleClick(index)}
toBeIncremented={incrementValue}
/>
));
}
<br />
{
appState.map(
({value}, index) => <MyLabel key={index} counter={value} />
);
}
</div>
);
}

Related

React: Check Array if string matches and set state

I have the following Array:
[
"/test/static/media/its9-odc_d.9d5de720.png",
"/test/static/media/its9-odc_m.178c1879.png",
"/test/static/media/its9-odc_w.5e70ca59.png",
"/test/static/media/its9-odc_y.8cf41473.png"
]
When I click on a Button, either _d, _m, _w or _y is saved in a React state (timeperiods).
I need a function which should save the string which matches my timeperiods in another React state (imageSource), so I can render the image.
This is what I tried so far but I always get returned 'yay' and when I try to setImageSource I get an error for infinite loop.
const imageForTimeperiod = () => {
images.forEach((img) => {
if (timeperiod && img.split('.')[0].includes(timeperiod)) {
console.log('yay');
// setImage(img);
}
})
};
{Object.entries(Timeperiods.timeperiods).map((entries) => {
return (
<Button onClick={() => setTimeperiod(entries[1].file)}>
{entries[0]}
</Button>
);
})}
In the end:
I click on Button 1 Day, it sets timeperiod to _d and I show the image /test/static/media/its9-odc_d.9d5de720.png.
You can pass the name of timeperiod on clicked button and then based on the name set the image and timePeriod state.
const { useState } = React;
const App = () => {
const [image, setImage] = useState(null);
const [timePeriod, setTimePeriod] = useState(null);
const images = [
"/test/static/media/its9-odc_d.9d5de720.png",
"/test/static/media/its9-odc_m.178c1879.png",
"/test/static/media/its9-odc_w.5e70ca59.png",
"/test/static/media/its9-odc_y.8cf41473.png"
];
const timePeriods = ['_d', '_m', '_w', '_y'];
const handleClick = (v) => {
const foundImage = images.find(name => name.includes(v));
setImage(foundImage || null);
setTimePeriod(v);
}
return <div>
{timePeriod}
<br />
{image && <img src={image} />}
<br />
{timePeriods.map((v, i) => {
return (
<button onClick={() => handleClick(v)}>
{v}
</button>
)
})}
</div>
}
ReactDOM.render(
<App />,
document.getElementById("root")
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to toggle class of a single element in a .map() function?

I am trying to toggle a class for a specific element inside a loop.
const ItemList: React.FC<ListItemUserProps> = (props) => {
const { items } = props;
const [showUserOpt, setShowUserOpt] = useState<boolean>(false);
function toggleUserOpt() {
setShowUserOpt(!showUserOpt);
}
const userOptVisible = showUserOpt ? 'show' : 'hide';
return (
<>
{items.map((t) => (
<React.Fragment key={t.userId}>
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={() => toggleUserOpt()}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}
</>
);
};
export default ItemList;
When I click on an element, the class toggles for every single one.
You can create another component that can have it's own state that can be toggled without effecting other sibling components' state:
Child:
const ItemListItem: React.FC<SomeInterface> = ({ item }) => {
const [show, setShow] = useState<boolean>(false);
const userOptVisible = show ? "show" : "hide";
const toggleUserOpt = (e) => {
setShow((prevState) => !prevState);
};
return (
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={toggleUserOpt}
onKeyDown={toggleUserOpt}
>
{item.userNav.firstName}
</div>
);
};
Parent:
const ItemList: React.FC<ListItemUserProps> = ({ items }) => {
return (
<>
{items.map((t) => (
<ItemListItem key={t.userId} item={t} />
))}
</>
);
};
If you simply adding classes to the element, I would keep it simple and use a handler to toggle the class using pure JS.
const handleClick = (e) => {
// example of simply toggling a class
e.currentTarget.classList.toggle('selected');
};
Demo:
const {
useState,
} = React;
// dummy data
const data = Array(20).fill(null).map((i, index) => `item ${(index + 1).toString()}`);
function App() {
const [items, setItems] = useState(data);
const handleClick = (e) => {
e.currentTarget.classList.toggle('selected');
};
return (
<div>
{items.map((item) => (
<button key={item} onClick={handleClick}>{item}</button>
))}
</div>
);
}
ReactDOM.render( <
App / > ,
document.getElementById("app")
);
.selected {
background: red;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I think it'd be best if you kept track of the index so that you could target a single item in your list. As it stands the boolean is going to change the styling for all as you haven't specified which one should get the className.
Add a useState hook to keep track of it like:
const [activeIndex, setActiveIndex] = useState(null);
Then create a new function:
function handleIndexOnClick(index) {
setActive(index);
}
Then in your map() function add index. You'll then need to pass index in to you className attribute and the onClick function. The end result for that bit should look like:
{items.map((t, index) => (
<React.Fragment key={t.userId}>
<div
className={`item ${activeIndex && items[activeIndex] ? 'show' : 'hide }`}
role="button"
tabIndex={0}
onClick={() => handleIndexOnClick(index)}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}

Can I render piece of a stateful component in react?

Is there any api that allow us to write code something like this:
const MyComponents = () => {
const [number, setNumber] = useState(0);
return {
Btn: <Button onPress={() => setNumber(number + 1)}>
{number}
</Button>,
Log: <p>{number}</p>
}
}
const Perent = () => <>
<div ...>
<MyComponents.Btn/>
...
...
</div>
<MyComponents.Log/>
</>
Some kind of ability to group some Component.And render them in different places...
Seems like this would be better achieved by using a Context.
E.g.
const { createContext, useState, useContext } = React;
const CountContext = createContext();
const CountContainer = ({ children }) => {
const [number, setNumber] = useState(0);
return <CountContext.Provider value={{ number, setNumber }}>
{children}
</CountContext.Provider>
};
const CountButton = () => {
const { number, setNumber } = useContext(CountContext);
return <button onClick={() => setNumber((c) => c + 1)}>
{number}
</button>;
};
const CountLog = () => {
const { number } = useContext(CountContext);
return <p>{number}</p>;
};
const SomeCountButtons = () => <div><CountButton /><CountButton /></div>;
const App = () => (<div>
<CountContainer>
<CountButton />
<CountLog />
</CountContainer>
<CountContainer>
<SomeCountButtons />
<CountLog />
</CountContainer>
</div>);
ReactDOM.render(<App />, document.getElementById('app'));
<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="app"></div>
Then any <CountButton>s or <CountLog>s that occur anywhere within the same <CountContainer> will be able to share their state.

How do I make one component talk to another when they are generated with .map() in React?

I have multiple components with the same module using map().
list.map((data, index) => <MyComponent key={index} value={d}/>)
Then <p> in each of MyComponent changes colors from green to red when it is clicked.
const MyComponent = ({value}) => {
const [clicked, setClicked] = useState(false);
const buttonOnClick = () => {
setClicked(true);
}
return (
<div>
<p style={clicked ? {color: 'green'} : {color: 'red'}}>{value}</p>
<button onClick={buttonOnClick}>click</button>
</div>
);
};
In this case, I would like to turn color of <p> in other MyComponent red when one of them are clicked.
How can I check the <p> state of other MyComponent?
You need to pass a callback into your child component, and have your parent component to store and control the state. Here's an example:
const list = [1, 2, 3];
const MyComponent = ({ value, clickedValue, onClick }) => {
const style = { color: clickedValue === value ? 'green' : 'red' };
return (
<div>
<p style={style}>{value}</p>
{/* Callback with the value */}
<button onClick={() => onClick(value)}>click</button>
</div>
);
};
const App = () => {
const [clickedValue, setClickedValue] = React.useState();
const handleClick = value => {
setClickedValue(value);
};
return React.Children.toArray(
list.map(value => (
<MyComponent
value={value}
clickedValue={clickedValue}
onClick={handleClick}
/>
))
);
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Probably the easiest option would be to lift state up into the parent component: https://reactjs.org/docs/lifting-state-up.html

Best way to change screen in React when URL not updated

I have an app I'm building in React where the url never updates despite each screen taking up the entire page.
What is the best way to render each screen when it's the screen to be viewed?
export const App = () => {
const [activeScreen, setActiveScreen] = useState("PAGE_1");
const goToScreen2 = () => setActiveScreen("PAGE_2");
const goToScreen3 = () => setActiveScreen("PAGE_3");
return (
<Wrapper>
{activeScreen === "PAGE_1" &&
<Page1 nextScreen={goToScreen2} />
}
{activeScreen === "PAGE_2" &&
<Page2 nextScreen={goToScreen3} />
}
{activeScreen === "PAGE_3" &&
<Page3 />
}
</Wrapper>
);
};
I would do something like this:
function Page1() {
return 'Page 1'
}
function Page2() {
return 'Page 2'
}
function Page3() {
return 'Page 3'
}
function Pager({
next,
prev,
children
}) {
return (
<div>
<button onClick={prev}>Prev</button>
{children}
<button onClick={next}>Next</button>
</div>
)
}
const pages = [Page1, Page2, Page3]
const App = () => {
const [activeScreen, setActiveScreen] = React.useState(0);
const goNext = () => setActiveScreen((activeScreen + 1) % pages.length);
const goBack = () => setActiveScreen((activeScreen - 1 + pages.length) % pages.length);
const ActivePage = pages[activeScreen]
return (
<Pager next={goNext} prev={goBack}>
<ActivePage />
</Pager>
);
};
ReactDOM.render( <App /> , document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root" />

Resources