How should I update individual items' className onClick in a list in a React functional component? - reactjs

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?

Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

Related

React createProtal called outsite a JSX component not updating the DOM

I am trying to render a dynamically generated react component in a react app using createProtal.
When I call createProtal from a class the component is not rendered.
Handler.ts the class the contains the business logic
export class Handler {
private element: HTMLElement | null;
constructor(selector: string) {
this.element = document.getElementById(selector);
}
attachedEvent() {
this.element?.addEventListener("mouseenter", () => {
let cancel = setTimeout(() => {
if (this.element != null)
this.attachUi(this.element)
}, 1000)
this.element?.addEventListener('mouseleave', () => {
clearTimeout(cancel)
})
})
}
attachUi(domNode: HTMLElement) {
createPortal(createElement(
'h1',
{className: 'greeting'},
'Hello'
), domNode);
}
}
Main.tsx the react component that uses Handler.ts
const handler = new Handler("test_comp");
export default function Main() {
useEffect(() => {
// #ts-ignore
handler.useAddEventListeners();
});
return (
<>
<div id="test_comp">
<p>Detect Mouse</p>
</div>
</>
)
}
However when I repleace attachUi function with the function below it works
attachUi(domNode: HTMLElement) {
const root = createRoot(domNode);
root.render(createElement(
'h1',
{className: 'greeting'},
'Hello'
));
}
What am I missing?
React uses something called Virtual DOM. Only components that are included in that VDOM are displayed to the screen. A component returns something that React understands and includes to the VDOM.
createPortal(...) returns exactly the same as <SomeComponent ... />
So if you just do: const something = <SomeComponent /> and you don't use that variable anywhere, you can not display it. The same is with createPortal. const something = createPortal(...). Just use that variable somewhere if you want to display it. Add it to VDOM, let some of your components return it.
Your structure is
App
-children
-grand children
-children2
And your portal is somewhere else, that is not attached to that VDOM. You have to include it there, if you want to be displayed.
In your next example using root.render you create new VDOM. It is separated from your main one. This is why it is displayed

Like Button with Local Storage in ReactJS

I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff

React: Design pattern that uses a Ref to set style of the parent component

I would like to ask if this is a sensible component design pattern in React.
Let's say I have the following components, App, ContentContainer and WithBlueBackground. The idea is to use a Ref to set a blue background on the ContentContainer with the WithBlueBackground component.
The simplified code would look something like this.
// App.js
export function App() => {
const contentContainerRef = useRef();
return (
<ContentContainer contentContainerRef={contentContainerRef}>
<WithBlueBackground contentContainerRef={contentContainerRef}>
</WithBlueBackground>
</ContentContainer>
)
}
// ContentContainer
export function ContentContainer(props) => {
return (
<div ref={props.contentContainerRef}>
// Some content
</div>
)
}
// WithBlueBackground
export function ContentContainer(props) => {
useEffect(() => {
if (props.containerRef && props.contentContainerRef.current) {
props.contentContainerRef.current.style.backgroundColor = 'blue';
}
}, [props.contentContainerRef])
return <>{ props.children }</>;
}
This way if I want to have a green background in the content container I can create a new component that sets this style without the ContentContainer having to know about this. This increases the composability of the code which is promote in the react docs.
Nevertheless, passing the refs is a bit ugly.
My question is, is this a sensible pattern and if not is there another way to achieve what I am trying to do here.
If it is a direct child, you could just pass an update function around:
// ContentContainer
export function ContentContainer(props) {
const [backgroundColor, setColor] = React.useState("white");
return (
<div style={{ backgroundColor }}>
<ChildComponent setColor={color => setColor(color)}>// Some content</ChildComponent>;
</div>
);
}
// WithBlueBackground
export function ChildComponent(props) {
React.useEffect(() => {
props.setColor("blue");
}, []);
return <>{props.children}</>;
}
If it is deeper nested, you could use the context API. The same principle applies.enter link description here

Jest testing library - multiple calls to load data on click event

I am trying to test a load more button call on an onClick fireEvent but I am having trouble simulating the click to trigger a load data.
component:
class Items extends Component {
// states
componentDidMount() {
this.getData()
}
getData() { ...
// get data from state - pagination # and data size
}
onLoadMore() {
// increment pagination & offset on states
this.getData()
}
render() {
return (
<div className='container'>
{items.map((item, i) => {
return (
<div className='item-box'>
// item info
</div>
)
}
)}
<button onClick={this.onLoadMore}>Load More</button>
</div>
)
}
}
test:
it('load more data on load more button click', () => {
const Items = require('./default').default
// set initial load values: initVals (2 items)
// set second call values: secondVals (4 items)
Items.prototype.getData = jest.fn()
Items.prototype.getData.mockReturnValue(initVals)
Items.prototype.getData.mockReturnValue(secondVals)
const { container } = render(
<Items
fields={{ loadMore: true }}
/>
)
const button = screen.getByText('Load More')
fireEvent.click(button)
expect(container.querySelectorAll('.item-box').length).toBe(2)
expect(container.querySelectorAll('.item-box').length).toBe(4)
})
So this only reads the last call, finding 4 items.
Calling .mockReturnValue() multiple times has only yielded me the last call instead of it consecutively. I know I am using it wrong but I can't figure out the sequence of running this. My goal is to initialize the component with first values (load 2 items), then on click, it loads more (4 items).
Help?
I think you need to call mockReturnValueOnce instead of mockReturnValue, and you definitely need to move your first assertion before clicking the event. Also, I think your second assertion should expect 6, not 4.
The order of operations for your test should be:
Set up mocks
Render component
Assert initial value on load
Simulate click event
Assert value after click.
Here is a simple example that demonstrates this concept:
// src/Demo.js
import React, { useState } from "react";
const Demo = ({ loadText }) => {
const [text, setText] = useState("");
return (
<div>
<button onClick={() => setText(loadText())}>Load</button>
<p data-testid="data">{text}</p>
</div>
);
};
export default Demo;
// src/Demo.test.js
import { fireEvent, render, screen } from "#testing-library/react";
import '#testing-library/jest-dom'
import Demo from "./Demo";
test("callbacks", () => {
const myMock = jest.fn();
myMock.mockReturnValueOnce(2);
myMock.mockReturnValueOnce(4);
render(<Demo loadText={myMock} />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("data")).toHaveTextContent("2");
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("data")).toHaveTextContent("4");
});
Also, the way you are mocking this function (Items.prototype.getData = jest.fn()) seems odd to me. I recommend you explore other options, such as (1) mocking out axios or similar library, (2) mocking out a redux store, or (3) mocking out props that you pass to this component.

How To do mapping of click , text and className in Redux's presentational componant

I am new to react-redux. I am trying to do mapping in Redux presentational component. However, I am failing to do so. My code is as following:
const ABC = ({isAOn, isBOn, isCOn, isDOn,onAClick, onBClick, onCClick, onDClick }) => {
const Array = [{click:'onAClick',style:'isAOn',text:'AAAA'},
{click:'onBClick',style:'isBOn',text:'BBBB'},
{click:'onCClick',style:'isCOn',text:'CCCC'},
{click:'onDClick',style:'isDOn',text:'DDDD'}]
return (
<div>
{Array.map((test) =>
<div onClick={() => test.click} className={({test.style})?'DIV-ON':'DIV-OFF'}>{test.text}</div>
)}
</div>
)
}
export default ABC
Note: 1) isAOn, isBOn are boolean, which are used to toggle className of component.
2) I have also tried writing onClick differently. For example, onClick = {test.click} etc.
3) I have run code without mapping, it works fine. However, it is creating very large amount of repetitive coding which I want to reduce using mapping.
4) It will be very helpful, if you provide solution by running above code in fiddle.
You want something like this:
import React from "react";
import ReactDOM from "react-dom";
const onAClick = () => {
alert("clicked");
};
const App = ({ isAOn, onAClick }) => {
const Array = [{ click: onAClick, style: "isAOn", text: "AAAA" }];
return (
<div>
{Array.map(test => (
<div
onClick={() => test.click()}
className={isAOn ? "DIV-ON" : "DIV-OFF"}
>
{test.text}
</div>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App onAClick={onAClick} />, rootElement);
Working example here.
A couple issues,
test.click is not a function. It is actually just a string.
Even if it was, you are not invoking the function. To do that you need to have () at the end of the call.
you have type.isAOn and according to what you posted type isnt defined anywhere
Another approach could be as follows.
Instead of deconstructing your props, you should keep it together. This will allow you use the string name in the array to find it within the props object and directly pass it into the onClick prop. This removes the need for placing an anonymous function in to call the function.
const ABC = props => {
const Array = [{click:'onAClick',style:'isAOn',text:'AAAA'},
{click:'onBClick',style:'isBOn',text:'BBBB'},
{click:'onCClick',style:'isCOn',text:'CCCC'},
{click:'onDClick',style:'isDOn',text:'DDDD'}]
return (
<div>
{Array.map((test) =>
<div onClick={props[test.click]} className={({type.isAOn})?'DIV-ON':'DIV-OFF'}>{test.text}</div>
)}
</div>
)
}
export default ABC
I didnt make any assumptions about the type.isAOn so I left it how it was but you can follow the same pattern that was done for the onClick to gain access to the props you are passing down.

Resources