React onClick cannot handle multiple methods - javascript

I have the following React component:
const ParentComponent = () => {
const [data, setData] = useState()
// handle these data here and display them
return <ChildComponent sendData={setData} data={data} />
}
const ChildComponent = ({ data, sendData }) => {
const toggleStrikethrough = e => {
e.target.style.textDecoration = e.target.style.textDecoration === 'line-through' ? '' : 'line-through'
const { booking } = e.currentTarget.dataset
sendData(booking)
}
return (
<div>
{data.map(booking => (
<button key={index} data-booking={booking.name} onClick={toggleStrikethrough}>
{booking.name}
</button>
))}
</div>
)
}
AS you can see I have parent component which receive the data from child component but my issue is with the child component, when I try to manipulate the style with sending data in same time it does not work, but only one of them work eg. if I remove sendData(booking) the styles change and if I remove the style manipulating in the same function toggleStrikethrough() then the data sent to parent component but they cannot work together, any idea why?

The issue is it's rerendering after the parent's state is getting updated.
When you do sendData it updates the state of the parent. And when the state updates it re-renders the component, including all the children.
Ideally, you'd want to style your components based on data stored in the state.

I think React is likely re-rendering your buttons without the text decorations.
What you probably want is to modify the elements in the data passed down to ChildComponent so that "selected"/"checked" is a part of the state (boolean probably works).
e.g. if your data currently looks like: [{ name: "foo" }, { name: "bar" }], you can manipulate it so that it becomes [{ name: "foo" }, { name: "bar", checked: true }].
So you can do something like this:
const ParentComponent = () => {
const [data, setData] = useState()
// handle these data here and display them
return <ChildComponent sendData={setData} data={data} />
}
const ChildComponent = ({ data, sendData }) => {
const toggleStrikethrough = e => {
const { booking } = e.currentTarget.dataset
sendData({ ...booking, checked: true })
}
return (
<div>
{data.map(booking => (
<button key={index} data-booking={booking.name} onClick={toggleStrikethrough} styles={{ textDecoration: booking.checked ? "line-through" : "initial"}}>
{booking.name}
</button>
))}
</div>
)
}

Related

How can i place values of mapped input element in a state array on the parent element

I have two components, the parent(name Parent) and child(name Child), on the parent, i map an array and render the child, so the child appears like 4 times(number of times the child is displayed based on the mapping), i have an input field on the Child component (which will be 1 input field for the rendered child component), i am basically trying to get the values of the input field from all the rendered Child component (4 been rendered based on the mapping) and then send it to my parent component (store it in a state on the parent component).
mock code
parent component
const Items = [1,2,3,4]
export const Parent= () => {
return (<div>
{Items.map((Item, index) => {
return (
<div key={index}>
<Child />
</div>
);
})}
</div>
)
}
child component
export const Child = () => {
const [Amount, setAmount] = useState();
return (
<input
value={Amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
)
}
sorry for the bad code formatting.
This is a mock of what it looks like
this should give a somewhat clear understanding or image of the issue, i basically want to get all the Amount on the 4 render children, place it in an array and send it to the Parent component (so i can call a function that uses all the amount in an array as an argument)
i tried to set the values of the Child component to a state on context (it was wrong, it kept on pushing the latest field values that was edited, i am new to react so i didnt understand some of the things that were said about state lifting
Congratulations, you've discovered the need for the Lifting State Up React pattern. Lift the "amount" state from the child component up to the parent component. The parent component holds all the state and provides it and a callback function down to children components via props.
Example:
import { useState } from "react";
import { nanoid } from "nanoid";
const initialState = Array.from({ length: 4 }, (_, i) => ({
id: nanoid(),
value: i + 1
}));
const Child = ({ amount, setAmount }) => {
return (
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
);
};
const Parent = () => {
const [items, setItems] = useState(initialState);
const setAmount = (id) => (amount) =>
setItems((items) =>
items.map((item) =>
item.id === id
? {
...item,
value: amount
}
: item
)
);
return (
<div>
{items.map((item) => (
<Child
key={items.id}
amount={item.value}
setAmount={setAmount(item.id)}
/>
))}
</div>
);
};
try following method:
const Items = [1, 2, 3, 4];
export default function Parent() {
return <Child items={Items} />;
}
export function Child(props) {
const [childItems, setChildItems] = useState(props.items);
const handleChange = (e, index) => {
let temp = childItems;
temp[index] = e.target.value;
setChildItems(temp);
};
return (
<div>
{childItems.map((item, index) => {
return (
<div key={index}>
<input
value={item}
onChange={(e) => handleChange(e, index)}
placeholder="Amount"
/>
</div>
);
})}
</div>
);
}```

Memoize a function in parent before passing it to children to avoid rerender React

I'm building a chat app, I have 3 main components from parent to child in this hierarchical order: Chat.js, ChatLine.js, EditMessage.js.
I have a function updateMessage in Chat component that I need to pass to the second child EditMessage, but it causes a rerender of every ChatLine when I click on Edit button and begin typing.
I can't figure out how to memoize it so it only causes a rerender on the ChatLine I'm editing.
It only works if I pass it to ChatLine as :
updateMessage={line.id === editingId ? updateMessage : null}
instead of :
updateMessage={updateMessage}
But I want to avoid that and memoize it instead so it doesn't cause a rerender after each letter I type while editing a message.
This is the whole code: (also available in CodeSandbox & Netlify)
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={line.id === editingId ? editValue : ""}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
updateMessage={updateMessage}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
updateMessage,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
updateMessage={updateMessage}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({
editValue,
setEditValue,
editingId,
setEditingId,
updateMessage,
}) => {
return (
<div>
<textarea
onKeyPress={(e) => {
if (e.key === "Enter") {
// prevent textarea default behaviour (line break on Enter)
e.preventDefault();
// updating message in DB
updateMessage(editValue, setEditValue, editingId, setEditingId);
}
}}
onChange={(e) => setEditValue(e.target.value)}
value={editValue}
autoFocus
/>
<button
onClick={() => {
setEditingId(null);
setEditValue("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
Use the useCallback React hook to memoize the updateMessage callback so it can be passed as a stable reference. The issue is that each time Chat is rendered when editValue state updates it is redeclaring the updateMessage function so it's a new reference and triggers each child component it's passed to to rerender.
import { useCallback } from 'react';
...
const updateMessage = useCallback(
(editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
// If updating messages state use functional state update to
// avoid external dependencies.
},
[]
);

How to access history prop from a parent component using react?

i have a method showInfo that is accessed by two components Child1 and Child2.
initially i had the showInfo method within Child1 component like below
const Child1 = ({history}: RouteComponentProps) => {
type Item = {
id: string,
name: string,
value: string,
};
const showInfo = (item: item) => {
const id = item.id;
const value = item.value;
const handleRouteChange = () => {
const path = value === 'value1' ? `/value1/?item=${itemId}` : `/value2/?item=${itemId}`;
history.push(path);
}
return (
<Button onClick={handleRouteChange}> Info </Button>
);
}
return (
<SomeComponent
onDone = {({ item }) => {
notify({
actions: showInfo(item)
})
}}
/>
);
}
the above code works. but now i have another component child2 that needs to use the same method showInfo.
the component child2 is like below
const Child2 = () => {
return (
<Component
onDone = {({ item }) => {
notify({
actions: showInfo(item)
})
}}
/>
);
}
Instead of writing the same method showInfo in Child2 component i thought of having it in different file from where child1 and child2 components can share the method showInfo.
below is the file with name utils.tsx that has showInfo method
export const showInfo = (item: item) => {
const id = item.id;
const value = item.value;
const handleRouteChange = () => {
const path = value === 'value1' ? `/value1/?item=${itemId}` :
`/value2/?item=${itemId}`;
history.push(path); //error here push is not identified as no
//history passed
}
return (
<Button onClick={handleRouteChange}> Info </Button>
);
}
return (
<SomeComponent
onDone = {({ item }) => {
notify({
actions: showInfo(item)
})
}}
/>
);
}
With the above, i get the error where i use history.push in showInfo method. push not defined.
this is because history is not defined in this file utils.tsx
now the question is how can i pass history from child1 or child2 components. or what is the other way that i can access history in this case.
could someone help me with this. thanks.
EDIT:
notify in child1 and child2 is coming from useNotifications which is like below
const useNotifications = () => {
const [activeNotifications, setActiveNotifications] = React.useContext(
NotificationsContext
);
const notify = React.useCallback(
(notifications) => {
const flatNotifications = flatten([notifications]);
setActiveNotifications(activeNotifications => [
...activeNotifications,
...flatNotifications.map(notification => ({
id: notification.id,
...notification,
});
},
[setActiveNotifications]
)
return notify;
}
While you could potentially create a custom hook, your function returns JSX. There's a discussion about returning JSX within custom hooks here. IMO, this is not so much a util or custom hook scenario, as it is a reusable component. Which is fine!
export const ShowInfo = (item: item) => { // capitalize
const history = useHistory(); // use useHistory
// ... the rest of your code
}
Now in Child1 and Child2:
return (
<SomeComponent
onDone = {({ item }) => {
notify({
actions: <ShowHistory item={item} />
})
}}
/>
);
You may have to adjust some code in terms of your onDone property and notify function, but this pattern gives you the reusable behavior you're looking for.

How to change the value of a prop of Sub Component passed as a column to React Table?

I am using React Table Library in Web Application.
I am passing a component as a column to the table. Columns and data for the table looks like below.
columns = [
{
Header: "Name",
accessor: "name"
},
{
Header: "Action",
accessor: "action"
}
];
sampleData = [
{
id: 1,
name: "Product one",
action: (
<TestComponent
id={1}
inProgress={false}
onClickHandler={id => this.onClickHandler(id)}
/>
)
},
{
id: 2,
name: "Product two",
action: (
<TestComponent
id={2}
inProgress={false}
onClickHandler={id => this.onClickHandler(id)}
/>
)
}
];
My TestComponent Looks like below.
const TestComponent = ({ id, inProgress, onClickHandler }) => {
return (
<div>
{inProgress ? (
<p>In Progress </p>
) : (
<button onClick={() => onClickHandler(id)}>click me</button>
)}
</div>
);
};
What my purpose is, When user click on click me button it needs to call a Backend API. At that time inProgress prop need to be true and it should pass to the table and need text as In Progress until API call completed.
I could be able to do it changing state as below.
onClickHandler(id) {
const newData = this.sampleData.map(data => {
if (data.id === id) {
return {
...data,
action: (
<TestComponent
id={1}
inProgress={true}
onClickHandler={id => this.onClickHandler(id)}
/>
)
};
}
return data;
});
this.setState({ data: newData });
// Here I Call the Backend API and after complete that need to show the click me button again.
setTimeout(() => {
this.setState({ data: this.sampleData });
}, 3000);
}
I could be able to achieve what I need But I am not satisfied the way I am changing the state. I need to know is there a better way of doing this without changing state like this.
You can use this StackBlitz Link for giving me a better solution. Thanks.
Instead of passing inProgress prop to TestComponent, you could maintain a local state in the TestComponent that is used to determine whether to show progress text or a button and only pass the id and onClickHanlder props to TestComponent.
When button is clicked in TestComponent, you could set the the local state of TestComponent to show the progress text and then call the onClickHandler function passed as prop, passing in the id prop and a callback function as arguments. This callback function will be called when API request is completed. This callback function is defined inside TestComponent and only toggles the local state of the TestComponent to hide the progress text and show the button again.
Change your TestComponent to as shown below:
const TestComponent = ({ id, onClickHandler }) => {
const [showProgress, setShowProgress] = React.useState(false);
const toggleShowProgress = () => {
setShowProgress(showProgress => !showProgress);
};
const handleClick = () => {
setShowProgress(true);
onClickHandler(id, toggleShowProgress);
};
return (
<div>
{showProgress ? (
<p>In Progress </p>
) : (
<button onClick={handleClick}>click me</button>
)}
</div>
);
};
i have used useState hook to maintain local state of the TestComponent as it is a functional component but you could use the same logic in a class component as well.
Change the TestComponent in sampleData array to only be passed two props, id and onClickHandler.
{
id: 1,
name: "Product one",
action: <TestComponent id={1} onClickHandler={this.onClickHandler} />
}
and change onClickHandler method in App component to:
onClickHandler(id, callback) {
// make the api request, call 'callback' function when request is completed
setTimeout(() => {
callback();
}, 3000);
}
Demo
Alternatively, you could make onClickHandler function in App component to return a Promise that is fulfilled when API request completes. This way you don't have to pass a callback function from TestComponent to onClickHandler method in App component.
Change onClickHandler method to:
onClickHandler(id) {
return new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
}
and change TestComponent to:
const TestComponent = ({ id, onClickHandler }) => {
const [showProgress, setShowProgress] = useState(false);
const toggleShowProgress = () => {
setShowProgress(showProgress => !showProgress);
};
const handleClick = () => {
setShowProgress(true);
onClickHandler(id)
.then(toggleShowProgress)
.catch(error => {
toggleShowProgress();
// handle the error
});
};
return (
<div>
{showProgress ? (
<p>In Progress </p>
) : (
<button onClick={handleClick}>click me</button>
)}
</div>
);
};
Demo

set state is not updating state

I am trying to use the state hook in my react app.
But setTodos below seems not updating the todos
link to my work: https://kutt.it/oE2jPJ
link to github: https://github.com/who-know-cg/Todo-react
import React, { useState } from "react";
import Main from "./component/Main";
const Application = () => {
const [todos, setTodos] = useState([]);
// add todo to state(todos)
const addTodos = message => {
const newTodos = todos.concat(message);
setTodos(newTodos);
};
return (
<>
<Main
addTodos={message => addTodos(message)}
/>
</>
);
};
export default Application;
And in my main.js
const Main = props => {
const input = createRef();
return (
<>
<input type="text" ref={input} />
<button
onClick={() => {
props.addTodo(input.current.value);
input.current.value = "";
}}
>
Add message to state
</button>
</>
);
};
I expect that every time I press the button, The setTodos() and getTodos() will be executed, and the message will be added to the todos array.
But it turns out the state is not changed. (still, stay in the default blank array)
If you want to update state of the parent component, you should pass down the function from the parent to child component.
Here is very simple example, how to update state with hook from child (Main) component.
With the help of a button from child component you update state of the parent (Application) component.
const Application = () => {
const [todos, setTodos] = useState([]);
const addTodo = message => {
let todosUpdated = [...todos, message];
setTodos(todosUpdated);
};
return (
<>
<Main addTodo={addTodo} />
<pre>{JSON.stringify(todos, null, 2)}</pre>
</>
);
};
const Main = props => {
const input = createRef();
return (
<>
<input type="text" ref={input} />
<button
onClick={() => {
props.addTodo(input.current.value);
input.current.value = "";
}}
>
Add message to state
</button>
</>
);
};
Demo is here: https://codesandbox.io/s/silent-cache-9y7dl
In Application.jsx :
You can pass just a reference to addTodos here. The name on the left can be whatever you want.
<Main addTodos={addTodos} />
In Main.jsx :
Since getTodo returns a Promise, whatever that promise resolves to will be your expected message.
You don't need to pass message as a parameter in Main, just the name of the function.
<Main addTodos={addTodos} />
You are passing addTodos as prop.
<Main
addTodos={message => addTodos(message)}
/>
However, in child component, you are accessing using
props.addTodo(input.current.value);
It should be addTodos.
props.addTodos(input.current.value);

Categories

Resources