so I have two buttons in my react App.js and when clicked I want my current state(list) to change to descending order according to which button i press(order by date or order by upvotes). My articles.js have the code that display the list of articles. But I'm having a hard time showing the list sorted after clicking the button tag found on my App.js which is the parent component.
import React, { useState } from 'react';
function Articles({articles}) {
const [list, setList] = useState(articles)
return (
<div className="card w-50 mx-auto">
<table>
<thead>
<tr>
<th>Title</th>
<th>Upvotes</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{list.map((a, i) =>
<tr data-testid="article" key={i}>
<td data-testid="article-title">{a.title}</td>
<td data-testid="article-upvotes">{a.upvotes}</td>
<td data-testid="article-date">{a.date}</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
export default Articles;
import React from 'react';
import './App.css';
import 'h8k-components';
import Articles from './components/Articles';
const title = "Sorting Articles";
function App({articles}) {
//set article to state then pass
const handleUpvotes = () => {
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
console.log(articles)
}
const handleDates = () => {
return
}
return (
<div className="App">
<h8k-navbar header={title}></h8k-navbar>
<div className="layout-row align-items-center justify-content-center my-20 navigation">
<label className="form-hint mb-0 text-uppercase font-weight-light">Sort By</label>
<button data-testid="most-upvoted-link" className="small" onClick={handleUpvotes}>Most Upvoted</button>
<button data-testid="most-recent-link" className="small" onClick={handleDates}>Most Recent</button>
</div>
<Articles articles={articles}/>
</div>
);
}
export default App;
The useState should be in the App
const [list, setList] = useState(articles)
//set article to state then pass
const handleUpvotes = () => {
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
setList(articles)
}
You should use the Effect Hook (https://reactjs.org/docs/hooks-effect.html).
useEffect(() => {
// articles was changed
}, [articles])
the problem that you are facing is that a misunderstanding of the React reactivity model, now lets take a look at this line
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
here you are successfully updating the array, but think about it. if React updated the UI whenever a variables inside the component updates that would be ineffective and problematic.
so in order to notify React about what has changed and it needs to update the UI, whenever you change a variable and you need the UI to update, you use useState from react.
and another point is that in your Article component you are expecting props, and calling useState at the time.
so moving the useState into the App component dose the work
const [list, setList] = useState(articles)
const handleUpvotes = () => {
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
setList(articles)
}
It is not clear where articles come from and if they need to be used in multiple components so I'll put them in context, that way you can use it anywhere in your application.
const ArticleContext = React.createContext();
const ArticleProvider = ({ children }) => {
const [articles, setArticles] = React.useState([
{ title: '1', upvotes: 1, date: 1 },
{ title: '3', upvotes: 3, date: 3 },
{ title: '2', upvotes: 2, date: 2 },
{ title: '4', upvotes: 4, date: 4 },
]);
const sortDirection = React.useRef(-1);
const sortByUpvotes = React.useCallback(() => {
//toggle sort direction
sortDirection.current = sortDirection.current * -1;
setArticles((articles) =>
[...articles].sort(
(a, b) =>
(a.upvotes - b.upvotes) * sortDirection.current
)
);
}, [setArticles]);
return (
<ArticleContext.Provider
value={{
articles,
sortByUpvotes,
}}
>
{children}
</ArticleContext.Provider>
);
};
function Articles() {
const { articles } = React.useContext(ArticleContext);
return (
<div className="card w-50 mx-auto">
<table>
<thead>
<tr>
<th>Title</th>
<th>Upvotes</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{articles.map((a, i) => (
<tr data-testid="article" key={i}>
<td data-testid="article-title">{a.title}</td>
<td data-testid="article-upvotes">
{a.upvotes}
</td>
<td data-testid="article-date">{a.date}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function App() {
const { sortByUpvotes } = React.useContext(
ArticleContext
);
return (
<div className="App">
<div className="layout-row align-items-center justify-content-center my-20 navigation">
<label className="form-hint mb-0 text-uppercase font-weight-light">
Sort By
</label>
<button
data-testid="most-upvoted-link"
className="small"
onClick={sortByUpvotes}
>
Most Upvoted
</button>
</div>
{/* no need to pass articles, they are in context */}
<Articles />
</div>
);
}
ReactDOM.render(
<ArticleProvider>
<App />
</ArticleProvider>,
document.getElementById('root')
);
<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="root"></div>
The next example shows how to sort using multiple fields:
const ArticleContext = React.createContext();
const ArticleProvider = ({ children }) => {
const [articles, setArticles] = React.useState([
{ title: '1', upvotes: 1, date: 3 },
{ title: '3', upvotes: 3, date: 3 },
{ title: '2', upvotes: 2, date: 4 },
{ title: '4', upvotes: 4, date: 2 },
]);
const sortDirection = React.useRef([-1, -1]);
const sortPriority = React.useRef([0, 1]);
const sortFunctions = React.useMemo(
() => [
(a, b) =>
(a.upvotes - b.upvotes) * sortDirection.current[0],
(a, b) =>
(a.date - b.date) * sortDirection.current[1],
],
[]
);
const sort = React.useCallback(() => {
setArticles((articles) =>
[...articles].sort((a, b) =>
sortPriority.current.reduce(
(result, fnIndex) =>
result === 0
? sortFunctions[fnIndex](a, b)
: result,
0
)
)
);
}, [sortFunctions]);
const setDirectionAndPriority = (num) => {
if (sortPriority.current[0] === num) {
sortDirection.current[num] =
sortDirection.current[num] * -1;
}
sortPriority.current = [
num,
...sortPriority.current.filter((n) => n !== num),
];
};
const sortByUpvotes = () => {
setDirectionAndPriority(0);
sort();
};
const sortByDate = () => {
setDirectionAndPriority(1);
sort();
};
return (
<ArticleContext.Provider
value={{
articles,
sortByUpvotes,
sortByDate,
}}
>
{children}
</ArticleContext.Provider>
);
};
function Articles() {
const { articles } = React.useContext(ArticleContext);
return (
<div className="card w-50 mx-auto">
<table>
<thead>
<tr>
<th>Title</th>
<th>Upvotes</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{articles.map((a, i) => (
<tr data-testid="article" key={i}>
<td data-testid="article-title">{a.title}</td>
<td data-testid="article-upvotes">
{a.upvotes}
</td>
<td data-testid="article-date">{a.date}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function App() {
const { sortByUpvotes, sortByDate } = React.useContext(
ArticleContext
);
return (
<div className="App">
<div className="layout-row align-items-center justify-content-center my-20 navigation">
<label className="form-hint mb-0 text-uppercase font-weight-light">
Sort By
</label>
<button
data-testid="most-upvoted-link"
className="small"
onClick={sortByUpvotes}
>
Most Upvoted
</button>
<button
data-testid="most-recent-link"
className="small"
onClick={sortByDate}
>
Most Recent
</button>
</div>
{/* no need to pass articles, they are in context */}
<Articles />
</div>
);
}
ReactDOM.render(
<ArticleProvider>
<App />
</ArticleProvider>,
document.getElementById('root')
);
<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="root"></div>
Related
I created a page that displays goods, next to every good there is a button with which you can select or deselect the good. My problem is, when I click on any of my buttons, all buttons change. How can I make it so that the callback function responsible for changing the state of the button only applies to the one that has been clicked?
import React from 'react';
import { useState } from 'react';
import 'bulma/css/bulma.css';
import './App.scss';
export const goods = [
'Dumplings',
'Carrot',
'Eggs',
'Ice cream',
'Apple',
'Bread',
'Fish',
'Honey',
'Jam',
'Garlic',
];
export const App: React.FC = () => {
const [selected, selectGood] = useState(true);
const [selectedGood, changeSelectedGood] = useState(goods[0])
const handleChange = (button: any) => {
changeSelectedGood(button.target.id);
return selectGood(!selected);
}
const clearAll = () => {
return selectGood(!selected);
}
return (
<main className="section container">
{!selected && (
<h1 className="title">No goods selected</h1>
)}
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
{selected && (
<h1 className="title is-flex is-align-items-center">
{selectedGood + ' is selected'}
<button
data-cy="ClearButton"
type="button"
className="delete ml-3"
onClick={clearAll}
/>
</h1>
)}
<table className="table">
<tbody>
{goods.map((good) => (
<tr
data-cy="Good"
key={good}
className={selected
? "has-background-success-light"
: ""
}
>
<td>
{good && (
<button
data-cy="AddButton"
type="button"
className={"button"}
id={good}
onClick={(good) => {handleChange(good)}}
>
{selected ? '-' : '+'}
</button>
)}
</td>
<td
data-cy="GoodTitle"
className="is-vcentered"
>
{good}
</td>
</tr>
))}
</tbody>
</table>
</main>
)
}
What you basically need is to have a state in which you can maintain the checked status and the value, on clicking the button, map over the status and update the status of element, that has been clicked by the user (in order to check which element user has clicked you either compare values or keep additional field such as id in the array). Below is a simpler(in sense of code) implementation of the same issue.
import * as React from 'react';
import './style.css';
import { useState } from 'react';
export default function App() {
const [goods, setGoods] = useState([
{ status: false, name: 'Dumplings' },
{ status: false, name: 'carrot' },
]);
const onClick = (event) => {
setGoods(
goods.map((item) => {
if (item === event.target.name)
return { ...item, status: !item.status };
else return item;
})
);
};
return (
<div>
{goods.map((good, idx) => {
return (
<div key={idx}>
<input
value={good.name}
type={'checkbox'}
onClick={onClick}
name={good.name}
/>
<label htmlFor={good.name}> {good.name}</label>
</div>
);
})}
</div>
);
}
Why you don't do like this:
import { useState } from 'react'
import 'bulma/css/bulma.css'
import './App.scss'
export const goods = [ 'Dumplings', 'Carrot', 'Eggs', 'Ice cream', 'Apple', 'Bread', 'Fish', 'Honey', 'Jam', 'Garlic']
const App = () => {
const [selectedGoods, setSelectedGoods] = useState<string[]>([])
const clearAll = () => {
return setSelectedGoods([])
}
const handleSelectGood = (good: string) => {
setSelectedGoods(
(selectedGoods: string[]) => selectedGoods.includes(good)
? selectedGoods.filter((selectedGood) => selectedGood !== good)
: [...selectedGoods, good]
)
}
return (
<main className="section container">
{/* header */}
{selectedGoods.length < 1
? <h1 className="title">No goods selected</h1>
: (
<h1 className="title is-flex is-align-items-center">
{selectedGoods.join(', ') + ' is selected'}
<button
onClick={clearAll}
className="delete ml-3"
type="button"
data-cy="ClearButton"
/>
</h1>
)}
{/* table */}
<table className="table">
<tbody>
{goods.map((good) => {
const isSelected = selectedGoods.includes(good)
return (
<tr
key={good}
className={isSelected ? "has-background-success-light" : ""}
data-cy="Good"
>
<td>
<button
id={good}
onClick={() => handleSelectGood(good)}
className={"button"}
data-cy="AddButton"
>
{isSelected ? '-' : '+'}
</button>
</td>
<td
className="is-vcentered"
data-cy="GoodTitle"
>
{good}
</td>
</tr>
)
})}
</tbody>
</table>
</main>
)
}
export default App
I am making a simple todo. I am fetching data from an API and I want to show all the items in a table by default. There will be 3 buttons - All, Complete and Incomplete which will show All, Completed and Incompleted todos table respectively. I have set states for completed and incompleted todos but can't wrap my head around how to perform conditional rendering and display different tables on different button clicks.
Below is my code -
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./style.css";
export default function App() {
const URL = 'https://jsonplaceholder.typicode.com/todos';
const [todo, setTodo] = useState([]);
const [completed, setCompleted] = useState([]);
const [incomplete, setIncomplete] = useState([]);
useEffect(()=>{
axios.get(URL)
.then(res=>setTodo(res.data));
},[])
const showCompleted = () =>{
const completeTask = todo.filter((items)=>items.completed===true);
setCompleted(completeTask);
}
const showIncomplete = () =>{
const incompleteTask = todo.filter((items)=>items.completed===false);
setIncomplete(incompleteTask);
}
return (
<div>
<h1>ToDos!</h1>
<button type="button">All</button>
<button type="button" onClick={showCompleted}>Completed</button>
<button type="button" onClick={showIncomplete}>Incomplete</button>
<hr />
<table>
<tr>
<th>ID</th>
<th>Title</th>
<th>Completed</th>
</tr>
{todo.map((items)=>
<tr key={items.id}>
<td>{items.id}</td>
<td>{items.title}</td>
<td><input type="checkbox" defaultChecked={items.completed ? true : false} /></td>
</tr>
)}
</table>
</div>
);
}
Instead of maintaining a separate state for each type have one type state that the buttons update when they're clicked. Add data attributes to the buttons to indicate what type they are and which can be picked up in the click handler.
Instead of mapping over the whole set of todos, call a function that filters out the set of data from the todo state that you need.
const { useEffect, useState } = React;
const URL = 'https://jsonplaceholder.typicode.com/todos';
function Example() {
const [todos, setTodos] = useState([]);
const [type, setType] = useState('all');
useEffect(()=>{
fetch(URL)
.then(res => res.json())
.then(data => setTodos(data));
}, []);
// Filter the todos depending on type
function filterTodos(type) {
switch(type) {
case 'completed': {
return todos.filter(todo => todo.completed);
}
case 'incomplete': {
return todos.filter(todo => !todo.completed);
}
default: return todos;
}
}
// Set the type when the buttons are clicked
function handleClick(e) {
const { type } = e.target.dataset;
setType(type);
}
// Call the filter function to get the
// subset of todos that you need based
// on the type
return (
<div>
<h1>ToDos!</h1>
<button
type="button"
className={type === 'all' && 'active'}
data-type="all"
onClick={handleClick}
>All
</button>
<button
type="button"
className={type === 'completed' && 'active'}
data-type="completed"
onClick={handleClick}
>Completed
</button>
<button
type="button"
className={type === 'incomplete' && 'active'}
data-type="incomplete"
onClick={handleClick}
>Incomplete
</button>
<hr />
<table>
<tr>
<th>ID</th>
<th>Title</th>
<th>Completed</th>
</tr>
{filterTodos(type).map(todo => {
const { id, title, completed } = todo;
return (
<tr key={id}>
<td>{id}</td>
<td>{title}</td>
<td>
<input
type="checkbox"
defaultChecked={completed ? true : false}
/>
</td>
</tr>
);
})}
</table>
</div>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
button { margin-right: 0.25em; }
button:hover { cursor:pointer; }
.active { background-color: lightgreen; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Keep two states, one to store the initial data and another one to keep track of actually displayed data.
Try like this:
function App() {
const URL = "https://jsonplaceholder.typicode.com/todos";
const [todo, setTodo] = React.useState([]);
const [view, setView] = React.useState([]);
React.useEffect(() => {
fetch(URL)
.then((res) => res.json())
.then((result) => {
setTodo(result);
setView(result);
});
}, []);
const showAll = () => {
setView(todo);
};
const showCompleted = () => {
const completeTask = todo.filter((items) => items.completed === true);
setView(completeTask);
};
const showIncomplete = () => {
const incompleteTask = todo.filter((items) => items.completed === false);
setView(incompleteTask);
};
return (
<div>
<h1>ToDos!</h1>
<button type="button" onClick={showAll}>
All
</button>
<button type="button" onClick={showCompleted}>
Completed
</button>
<button type="button" onClick={showIncomplete}>
Incomplete
</button>
<hr />
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Completed</th>
</tr>
</thead>
<tbody>
{view.map((items) => (
<tr key={items.id}>
<td>{items.id}</td>
<td>{items.title}</td>
<td>
<input
type="checkbox"
defaultChecked={items.completed ? true : false}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
<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 class='react'></div>
You can use useMemo to prepare the data to display based on some conditions/filters/search/ordering/ anything else.
So few steps to achieve that:
Optional, declare some object outside of the component to hold some constants. Maybe I choosed a poor name for that but the idea itself should be ok. FILTER_COMPLETED in the code.
Add a useState variable to hold active filter for this specific area. const [filterCompleteMode, setFilterCompleteMode] = useState(...) in the code.
Add a useMemo variable that will prepare the data to display. You can apply some ordering or additinal filtering here. todosToDisplay in the code.
Modify your JSX a bit, change <button>s and todo to todosToDisplay.
const { useState, useMemo, useEffect } = React;
const FILTER_COMPLETED = {
All: "ALL",
Complete: "COMPLETE",
Incomplete: "INCOMPLETE"
};
function App() {
const URL = "https://jsonplaceholder.typicode.com/todos";
const [todos, setTodos] = useState([]);
const [filterCompleteMode, setFilterCompleteMode] = useState(
FILTER_COMPLETED.All
);
const todosToDisplay = useMemo(() => {
if (!todos) return [];
switch (filterCompleteMode) {
case FILTER_COMPLETED.All:
return todos;
case FILTER_COMPLETED.Incomplete:
return todos.filter((x) => x.completed === false);
case FILTER_COMPLETED.Complete:
return todos.filter((x) => x.completed === true);
default:
return todos;
}
}, [todos, filterCompleteMode]);
useEffect(() => {
fetch(URL)
.then((res) => res.json())
.then((data) => setTodos(data));
}, []);
const onCompleteFilterClick = (e) => {
setFilterCompleteMode(e.target.dataset.mode);
};
return (
<div>
<h1>ToDos!</h1>
<button
type="button"
data-mode={FILTER_COMPLETED.All}
onClick={onCompleteFilterClick}
>
All
</button>
<button
type="button"
data-mode={FILTER_COMPLETED.Complete}
onClick={onCompleteFilterClick}
>
Completed
</button>
<button
type="button"
data-mode={FILTER_COMPLETED.Incomplete}
onClick={onCompleteFilterClick}
>
Incomplete
</button>
<hr />
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Completed</th>
</tr>
</thead>
<tbody>
{todosToDisplay.map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.title}</td>
<td>
<input
type="checkbox"
defaultChecked={item.completed ? true : false}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>
Create state:
const [whatShow, setWhatShow] = useState('All').
When you click on button change this state
next:
{todo.map((items)=>
{items.completed === whatShow && <tr key={items.id}>
<td>{items.id}</td>
<td>{items.title}</td>
<td><input type="checkbox" defaultChecked={items.completed ? true : false} /></td>
</tr>}
)}
something like this
I know this is common question and something is wrong with state, but I still need help with understanding of all features of Redux and Redux-toolkit. So, in my PET project I'm trying to edit an invoice, but UI isn't updating, however it logs to the console changes which you make (here is screenshot).
And if I try to edit items(item name or unit costs or unit) it shows an error.
It's kind of 2 problems in one post, but related to the same topic :)
Now the code.
invoice-slice.js file with editInvoice reducer where might be the problem of updating the state, but i don't know where it can be:
editInvoice(state) {
const existingItem = state.invoices;
existingItem.map((item) => {
if (existingItem.id === item.id) {
return {
id: item.id,
bill_from: item.billFrom,
bill_from_info: item.billFromInfo,
bill_to: item.billTo,
bill_to_info: item.billToInfo,
invoice_num: item.invoiceNumber,
status: item.status,
order_date: item.order_date,
ITEMS: [...item.ITEMS],
};
}
return item;
});
},
EditInvoice.js file:
import React from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import classes from "./EditInvoice.module.css";
import EditInvoiceItem from "./EditInvoiceItem";
const EditInvoice = () => {
const { invoiceId } = useParams();
const invoices = useSelector((state) => state.invoice.invoices);
const invoice = invoices.find((invoice) => invoice.id === invoiceId);
return invoice ? (
<EditInvoiceItem
invoiceNumber={invoice.invoice_num}
billFrom={invoice.bill_from}
billFromInfo={invoice.bill_from_info}
billTo={invoice.bill_to}
billToInfo={invoice.bill_to_info}
status={invoice.status}
orderDate={invoice.order_date}
items={invoice.ITEMS}
itemName={invoice.item_name}
unitCosts={invoice.unit_costs}
units={invoice.units}
/>
) : (
<div className={classes.centered}>Invoice Not Found.</div>
);
};
export default EditInvoice;
EditInvoiceItem.js file:
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { uiActions } from "../../store/ui-slice";
import classes from "./AddInvoiceItem.module.css";
import { useFormik } from "formik";
import Wrapper from "../../UI/Wrapper";
import Card from "../../UI/Card";
import Footer from "../../UI/Footer";
import Button from "../../UI/Button";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faCalendar } from "#fortawesome/free-solid-svg-icons";
import { faEllipsis } from "#fortawesome/free-solid-svg-icons";
import { invoiceActions } from "../../store/invoice-slice";
import { useNavigate } from "react-router-dom";
const EditInvoiceItem = (props) => {
const navigate = useNavigate();
const date = new Date();
const options = ["Pending", "Shipped", "Delivered"];
const inputs = [{ item_name: "", unit_costs: "", unit: "" }];
const [startDate, setStartDate] = useState(date);
// const [startDate, setStartDate] = useState(props.orderDate || date);
const [selectedOption, setSelectedOption] = useState(
props.status || options[0]
);
const [listItems, setListItems] = useState(props.items || inputs);
// console.log(props.orderDate.toJSON());
const optionClickHandler = (value) => () => {
setSelectedOption(value);
dispatch(uiActions.toggleMoreOptions());
};
const editInvoiceHandler = (invoice) => {
console.log(invoice);
dispatch(
invoiceActions.editInvoice({
id: invoice.id,
invoiceNumber: invoice.invoiceNumber,
billFrom: invoice.billFrom,
billFromInfo: invoice.billFromInfo,
billTo: invoice.billTo,
billToInfo: invoice.billToInfo,
status: selectedOption,
order_date: startDate.toLocaleDateString(),
ITEMS: [...updateValuesOnSubmit()],
})
);
};
const formikEditInvoice = useFormik({
initialValues: {
invoiceNumber: props.invoiceNumber,
billFrom: props.billFrom,
billFromInfo: props.billFromInfo,
billTo: props.billTo,
billToInfo: props.billToInfo,
status: props.status,
order_date: props.orderDate,
item_name: props.itemName,
unit_costs: props.unitCosts,
units: props.units,
},
onSubmit: (val) => {
editInvoiceHandler(val);
navigate("/invoices", { replace: true });
},
});
const dispatch = useDispatch();
const toggleMoreOptions = () => {
dispatch(uiActions.toggleMoreOptions());
};
const showOtherOptions = useSelector(
(state) => state.ui.selectMoreOptionsIsVisible
);
let counter = 1;
const addItemHandler = () => {
setListItems(listItems.concat({ item_name: "", unit_costs: "", unit: "" }));
};
const updateItemHandler = (index, inputName, value) => {
listItems[index] = { ...listItems[index], [inputName]: value };
};
const updateValuesOnSubmit = () => {
return listItems;
};
const navigateBack = () => {
navigate(-1);
};
return (
<form onSubmit={formikEditInvoice.handleSubmit}>
<Wrapper isShrinked={props.isShrinked}>
<Card>
<div className={classes.content}>
<div className={classes["buttons-wrapper"]}>
<button
type="button"
className={classes["cancel-btn"]}
onClick={navigateBack}
>
Cancel
</button>
<Button>Save</Button>
</div>
<div className={classes["invoice-info-wrapper"]}>
<div className={classes["invoice-info"]}>
<h3>Invoice Info</h3>
<input
placeholder="Number"
type="text"
name="invoiceNumber"
id="invoiceNumber"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.invoiceNumber}
onBlur={formikEditInvoice.handleBlur}
></input>
</div>
<div className={classes["right-side-column"]}>
<div className={classes["order-status"]}>
<span>Order Status: </span>
<div className={classes.buttons}>
{showOtherOptions && (
<ul className={classes.options}>
{options.map((option, index) => (
<li onClick={optionClickHandler(option)} key={index}>
{option}
</li>
))}
</ul>
)}
<button type="button" className={classes.status}>
{selectedOption}
</button>
<button
type="button"
className={classes.dots}
onClick={toggleMoreOptions}
>
<FontAwesomeIcon icon={faEllipsis} />
</button>
</div>
</div>
<div className={classes["order-date"]}>
<span>Order Date:</span>
<DatePicker
className={classes["order-date-input"]}
selected={startDate}
onChange={(val) => setStartDate(val)}
/>
<FontAwesomeIcon
icon={faCalendar}
className={classes.calendar}
></FontAwesomeIcon>
</div>
</div>
</div>
<div className={classes["order-bills"]}>
<div className={classes["bill-from"]}>
<input
placeholder="Bill From"
type="text"
name="billFrom"
id="billFrom"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billFrom}
onBlur={formikEditInvoice.handleBlur}
></input>
<textarea
placeholder="Bill From Info"
name="billFromInfo"
id="billFromInfo"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billFromInfo}
onBlur={formikEditInvoice.handleBlur}
></textarea>
</div>
<div className={classes["bill-to"]}>
<input
placeholder="Bill To"
type="text"
name="billTo"
id="billTo"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billTo}
onBlur={formikEditInvoice.handleBlur}
></input>
<textarea
placeholder="Bill To Info"
name="billToInfo"
id="billToInfo"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billToInfo}
onBlur={formikEditInvoice.handleBlur}
></textarea>
</div>
</div>
<div className={classes["table-wrapper"]}>
<table>
<colgroup>
<col className={classes.col1}></col>
<col className={classes.col2}></col>
<col className={classes.col3}></col>
<col className={classes.col4}></col>
<col className={classes.col5}></col>
<col className={classes.col6}></col>
</colgroup>
<thead>
<tr>
<td className={classes["more-padding"]}>#</td>
<td>Item Name</td>
<td>Unit Costs</td>
<td>Unit</td>
<td>Price</td>
<td></td>
</tr>
</thead>
<tbody>
{listItems.map((item, index) => (
<tr data-1={item} key={index}>
<td className={classes["more-padding"]}>{counter++}</td>
<td>
<input
placeholder="Item Name"
className={classes.inputs}
name="itemName"
id="itemName"
onChange={(e) =>
updateItemHandler(
index,
"item_name",
e.currentTarget.value
)
}
value={item.item_name}
onBlur={formikEditInvoice.handleBlur}
></input>
</td>
<td>
<input
placeholder="Unit Costs"
className={classes.inputs}
name="unitCosts"
id="unitCosts"
onChange={(e) =>
updateItemHandler(
index,
"unit_costs",
e.currentTarget.value
)
}
value={item.unit_costs}
onBlur={formikEditInvoice.handleBlur}
></input>
</td>
<td>
<input
placeholder="Unit"
className={classes.inputs}
name="unit"
id="unit"
onChange={(e) =>
updateItemHandler(
index,
"unit",
e.currentTarget.value
)
}
value={item.unit}
onBlur={formikEditInvoice.handleBlur}
></input>
</td>
<td>0</td>
<td></td>
{/* There should be dynamic values later */}
</tr>
))}
</tbody>
</table>
<div className={classes["add-item-btn"]}>
<button
onClick={addItemHandler}
type="button"
className={classes["add-item-btn"]}
>
Add Item
</button>
</div>
<div className={classes.total}>
<p className={classes["sub-total"]}>
<span>Sub Total: </span>
<span>$0</span>
{/* Dynamic value later here */}
</p>
<div className={classes["total-vat"]}>
<span>Total Vat:</span>
<div className={classes["total-sum"]}>
<span className={classes["input-wrapper"]}>
<input type="text" defaultValue="10"></input>
<span>%</span>
</span>
<span className={classes.sum}>$0</span>
{/* Dynamic value later here */}
</div>
</div>
<div className={classes["grand-total"]}>
<h3>Grand Total</h3>
<div className={classes.input}>
<input type="text" defaultValue="$"></input>
<span>0</span>
{/* Dynamic value later here */}
</div>
</div>
</div>
</div>
<div className={classes.dummy}></div>
</div>
</Card>
<Footer />
</Wrapper>
</form>
);
};
export default EditInvoiceItem;
In my project I'm using Formik to listen for input changes, but in listening to items changes I'm not using it, because i had an issue with that and one guy suggested me that code.
So, I'm facing 2 issues:
UI doesn't update the changes, but i can see in console that changes are made(probably the code in invoice-slice.js is wrong);
When i click and want to change the inputs of Item Name, Unit Costs and Unit i get an error in console and can't change anything.
Please, try helping me and explaining what can cause such problems!
P.S. here is my github repo - https://github.com/stepan-slyvka/test-project
P.P.S. and here is CodeSandbox -
Issues
The main issue is that the editInvoice reducer function isn't consuming an action and its payload, so the passed updated invoice data isn't referenced, and the array mapping result isn't used.
Additionally, the EditInvoice component isn't passing along the invoice id to the EditInvoiceItem (*or EditInvoiceItem isn't capturing it from the path parameters. The invoice.id isn't populated into the form data so it's also not passed along to editInvoiceHandler to be dispatched to the store. This causes the array mapping to fail to find the invoice object that needs to be updated.
Solution
EditInvoice
EditInvoice needs to pass invoice.id as a prop to EditInvoiceItem. It would be quite a bit more clean and easier to maintain to simply pass the entire invoice object though.
const EditInvoice = () => {
const { invoiceId } = useParams();
const invoices = useSelector((state) => state.invoice.invoices);
const invoice = invoices.find((invoice) => invoice.id === invoiceId);
return invoice ? (
<EditInvoiceItem
id={invoice.id} // <-- passed here
invoiceNumber={invoice.invoice_num}
billFrom={invoice.bill_from}
billFromInfo={invoice.bill_from_info}
billTo={invoice.bill_to}
billToInfo={invoice.bill_to_info}
status={invoice.status}
orderDate={invoice.order_date}
items={invoice.ITEMS}
itemName={invoice.item_name}
unitCosts={invoice.unit_costs}
units={invoice.units}
/>
) : (
<div className={classes.centered}>Invoice Not Found.</div>
);
};
EditInvoiceItem
EditInvoiceItem should initialize the form state with the invoice id.
const formikEditInvoice = useFormik({
initialValues: {
id: props.id, // <-- pass id value...
invoiceNumber: props.invoiceNumber,
billFrom: props.billFrom,
billFromInfo: props.billFromInfo,
billTo: props.billTo,
billToInfo: props.billToInfo,
status: props.status,
order_date: props.orderDate,
item_name: props.itemName,
unit_costs: props.unitCosts,
units: props.units,
},
onSubmit: (val) => {
editInvoiceHandler(val); // <-- ...so it's passed along here
navigate("/invoices", { replace: true });
},
});
invoice-slice
The editInvoice case reducer should consume both the current state and the dispatched action so the payload containing the invoice data can be accessed.
editInvoice(state, action) {
const { payload } = action;
state.invoices = state.invoices.map((item) =>
item.id === payload.id
? {
...item,
bill_from: payload.billFrom,
bill_from_info: payload.billFromInfo,
bill_to: payload.billTo,
bill_to_info: payload.billToInfo,
invoice_num: payload.invoiceNumber,
status: payload.status,
order_date: payload.order_date,
ITEMS: payload.ITEMS.slice()
}
: item
);
}
I've got a react app. And I was making filter section where user puts max price of article.
Now everything works, only thing that is, page is rerendering on every single input in input box, even there is an button.
What would be the way to stop rerendering of a page? I want it to rerender only if button is pressed - so I only want to remove this rerendering when user inputs a price. Eg. User want to input price 200000 but page refreshes when he types 2,0,0 etc. I want to be able to input 200000 without refreshing only if button is pressed.
Thanks!
Here is my dashboard.js
const Dashboard = () => {
const form = useRef();
const checkBtn = useRef();
const pageSearchParam = new URLSearchParams(window.location.search);
const pageParam = Number(pageSearchParam.get('page')) || 1;
const maxPriceSearchParam = new URLSearchParams(window.location.search);
const maxPriceParam = Number(maxPriceSearchParam.get('maxprice'));
const parsedUrl = new URL(window.location.href);
const filterNameInURL = parsedUrl.searchParams.get('filter');
const [content, setContent] = useState([]);
const [maxPrice, setMaxPrice] = useState(maxPriceParam || []);
// eslint-disable-next-line
const [page, setPage] = useState(pageParam);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (filterNameInURL) {
const fetchFiltered = async () => {
const res = await ArticleService.filterByName(filterNameInURL, page);
const { count, rows } = await res.data;
setTotal(count);
setContent(rows);
};
fetchFiltered();
} else if (maxPrice) {
const fetchWithMaxPrice = async () => {
const res = await ArticleService.filterByMaxPrice(maxPrice, page);
const { count, rows } = await res.data;
setTotal(count);
setContent(rows);
};
fetchWithMaxPrice();
} else {
const fetchPosts = async () => {
const res = await ArticleService.articles(page);
const { count, rows } = await res.data;
setTotal(count);
setContent(rows);
};
fetchPosts();
}
}, [filterNameInURL, page, maxPrice]);
const onChangeMaxPrice = (e) => {
const maxprice = e.target.value;
setMaxPrice(maxprice);
};
const handleFilter = async (e) => {
e.preventDefault();
form.current.validateAll();
setLoading(true);
if (checkBtn.current.context._errors.length === 0) {
try {
onsubmit = () => {
maxPriceSearchParam.set('maxprice', maxPrice);
window.location.search = maxPriceSearchParam.toString();
};
} catch (error) {
console.log(error);
}
} else {
setLoading(false);
}
};
const render = (item, index) => {
return (
<tr key={index}>
<td className='text-center'>
<div key={item.id}>
<img
src={`${item.pictures}`}
alt='picture'
className='rounded'
></img>
</div>
</td>
<td className='text-center'>
<div key={item.id}>
<h4>{item.descr}</h4>
<br></br>
<h6 className='text-left'>No of sqm: {item.sqm}m2</h6>
<div className='text-left'>
<small className='text-left'>
{' '}
<a href={item.link} target='_blank' rel='noopener noreferrer'>
Show on page
</a>
</small>
</div>
</div>
</td>
<td className='text-center'>
<div key={item.id}>
<h4>{item.price}</h4>
<small className='text-left'>Price per m2: {item.ppm2}</small>
</div>
</td>
<td className='text-center'>
<div key={item.id}>
<Link to={`/article/${item.id}`}>
<h4>Show</h4>
</Link>
</div>
</td>
</tr>
);
};
const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
const renderHeader = () => {
if (filterNameInURL) {
return (
<h4 className='text-center'>
Total {total} articles on
{capitalize(filterNameInURL)}
</h4>
);
} else {
return (
<h4 className='text-center'>
Total {total} articles in DB
</h4>
);
}
};
return (
<div>
<div className='container'>
{renderHeader()}
<Form onSubmit={handleFilter} ref={form}>
Filters <br></br> Max price:
<input
type='text'
className='form text-center'
placeholder='npr. 120000'
aria-describedby='basic-addon2'
value={maxPrice}
onChange={onChangeMaxPrice}
/>
<button className='btn btn-primary btn-block w-25' disabled={loading}>
{loading && (
<span className='spinner-border spinner-border-sm'></span>
)}
<span>Filter</span>
</button>
<CheckButton style={{ display: 'none' }} ref={checkBtn} />
</Form>
<div className='table-responsive'>
<table className='table'>
<thead className='thead-dark'>
<tr>
<th className='text-center' scope='col'>
Picture
</th>
<th className='text-center' scope='col'>
Description
</th>
<th className='text-center w-25' scope='col'>
Price
</th>
<th className='text-center' scope='col'>
Offer
</th>
</tr>
</thead>
<tbody>{content.map(render)}</tbody>
</table>
</div>
</div>
<div className='mb-5 text-center'>
<Pagination
totalPages={Math.ceil(total / 10)}
onPageChange={(e, d) => {
pageSearchParam.set('page', d.activePage);
window.location.search = pageSearchParam.toString();
}}
activePage={page}
/>
</div>
</div>
);
};
export default Dashboard;
You are changing state on onChange function. So it forces re-render on every time when you type a letter in input box.
Take a new state and put that in useEffect instead of maxPrice. Set this state on buttonPress.
useEffect(() => {
// all code here
}, [filterNameInURL, page, newState]);
const onButtonPress () => {
setNewState(maxPrice)
}
Following document will help you to understand it in more details.
useEffect doc
I have created a component that sorts and filters an HTML table. The functionality is correct but I have a problem where my table renders "No asset records found." but when I click on one of the headers it displays the contents of the data array in state. I am truly stuck and confused on this strange behaviour. I think the problem might be with the filterAssets function because if I change from this:
let filterAssets = this.state.data.filter(a => {
return a.name.toLowerCase().indexOf(this.state.search) !== -1
})
to this:
let filterAssets = this.props.assetManagement.filter(a => {
return a.name.toLowerCase().indexOf(this.state.search) !== -1
})
Here is the code below if it helps
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { getAssetManagement } from '../../actions/asset-management'
class AssetManagement extends Component {
static propTypes = {
assetManagement: PropTypes.array.isRequired,
getAssetManagement: PropTypes.func.isRequired
}
componentDidMount() {
this.props.getAssetManagement()
}
state = {
name: '',
search: '',
data: []
}
sortBy = this.sortBy.bind(this)
compareBy = this.compareBy.bind(this)
onSubmit = e => {
e.preventDefault()
}
onChange = e =>
this.setState({
[e.target.name]: e.target.value
})
updateSearch = e =>
this.setState({
search: e.target.value.substr(0, 20)
})
compareBy(key) {
return (a, b) => {
if (a[key] < b[key]) return -1
if (a[key] > b[key]) return 1
return 0
}
}
sortBy(key) {
let arrayCopy = [...this.props.assetManagement]
this.state.data.sort(this.compareBy(key))
this.setState({ data: arrayCopy })
}
render() {
let filterAssets = this.state.data.filter(a => {
return a.name.toLowerCase().indexOf(this.state.search) !== -1
})
return (
<Fragment>
{/* Search input */}
<div class="input-group mb-1">
<div class="input-group-prepend">
<span class="input-group-text btn-secondary">
<i class="fas fa-search" />
</span>
</div>
<input
className="form-control"
type="text"
placeholder="Search Asset"
onChange={this.updateSearch.bind(this)}
value={this.state.search}
/>
</div>
{/* Asset management table */}
<div className="table-responsive">
<table className="table table-bordered text-center">
<thead>
<tr>
<th onClick={() => this.sortBy('id')}>ID</th>
<th onClick={() => this.sortBy('name')}>Name</th>
</tr>
</thead>
<tbody>
{filterAssets != 0 ? (
filterAssets.map(a => (
<tr key={a.id}>
<td>{a.id}</td>
<td>{a.name}</td>
</tr>
))
) : (
<tr>
<td colSpan={6}>No asset records found.</td>
</tr>
)}
</tbody>
</table>
</div>
</Fragment>
)
}
}
const mapStateToProps = state => ({
assetManagement: state.assetManagement.assetManagement
})
export default connect(
mapStateToProps,
{ getAssetManagement }
)(AssetManagement)
Change filterAssets != 0 to filterAssets.length > 0
One first render:
let filterAssets = this.state.data.filter(a => {
return a.name.toLowerCase().indexOf(this.state.search) !== -1
})
Your this.state.data is empty, only this.props.assetManagement available if you handle redux properly so no wonder it you cannot get anything from filtering.
Btw: filterAssets != 0 is absolutely wrong, so go ahead and change this line first.
When you use the alternative syntax for a React Component without using a constructor you no longer have access to props. So if you go back to using a standard constructor the problem disappears, e.g.:
constructor(props) {
super(props);
this.state = {
name: "",
search: "",
data: this.props.assetManagement
};
this.sortBy = this.sortBy.bind(this);
this.compareBy = this.compareBy.bind(this);
}
The real problem you have here is that you have two source of data: state.data and props.assetManagement - you retrieve from redux and get newest data from props.assetManagement, but when you need to trigger sorting, you make a copy to state.data. Then problem arises since you don't copy from props.assetManagement to state.data until you trigger sortBy function.
A solution for that is to get rid of state.data and store the sorting key in state. You can update, reset that key value, and sorting logic should be apply to props.assetManagement only:
class AssetManagement extends Component {
static propTypes = {
assetManagement: PropTypes.array.isRequired,
getAssetManagement: PropTypes.func.isRequired
}
componentDidMount() {
this.props.getAssetManagement()
}
state = {
name: '',
search: '',
sortingKey: ''
}
sortBy = this.sortBy.bind(this)
compareBy = this.compareBy.bind(this)
onSubmit = e => {
e.preventDefault()
}
onChange = e =>
this.setState({
[e.target.name]: e.target.value
})
updateSearch = e =>
this.setState({
search: e.target.value.substr(0, 20)
})
compareBy(key) {
return (a, b) => {
if (a[key] < b[key]) return -1
if (a[key] > b[key]) return 1
return 0
}
}
sortBy(key) {
if (key !== this.state.sortingKey) {
this.setState({ sortingKey: key });
}
}
render() {
let sortAssets = !!this.state.sortingKey ?
this.props.assetManagement.sort(this.compareBy(this.state.sortingKey)) :
this.props.assetManagement;
let filterAssets = sortAssets.filter(a => {
return a.name.toLowerCase().indexOf(this.state.search) !== -1
});
return (
<Fragment>
{/* Search input */}
<div class="input-group mb-1">
<div class="input-group-prepend">
<span class="input-group-text btn-secondary">
<i class="fas fa-search" />
</span>
</div>
<input
className="form-control"
type="text"
placeholder="Search Asset"
onChange={this.updateSearch.bind(this)}
value={this.state.search}
/>
</div>
{/* Asset management table */}
<div className="table-responsive">
<table className="table table-bordered text-center">
<thead>
<tr>
<th onClick={() => this.sortBy('id')}>ID</th>
<th onClick={() => this.sortBy('name')}>Name</th>
</tr>
</thead>
<tbody>
{filterAssets != 0 ? (
filterAssets.map(a => (
<tr key={a.id}>
<td>{a.id}</td>
<td>{a.name}</td>
</tr>
))
) : (
<tr>
<td colSpan={6}>No asset records found.</td>
</tr>
)}
</tbody>
</table>
</div>
</Fragment>
)
}
}
Sample code: https://codesandbox.io/s/n91pq7073l