I have a component that displays a list of Cards. I'm trying to sort the table rows but running into some issues. When i go to the page i'm getting the following error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
and it's pointing to this line
setData(_.sortBy(filteredData.reverse()));
here's my full component code. can anyone see a problem with what I'm trying to do?
import React, { useState } from "react";
import Search from "./Search";
import TimeAgo from "react-timeago";
import { useSelector, useDispatch, connect } from "react-redux";
import { Table } from "semantic-ui-react";
import { searchChange } from "../reducers/searchReducer";
import _ from "lodash";
// import { useField } from "../hooks";
const searchCards = ({ baseball, search }) => {
return search
? baseball.filter(a =>
a.title[0].toLowerCase().includes(search.toLowerCase())
)
: baseball;
};
const Cards = props => {
const [column, setColumn] = useState(null);
const [direction, setDirection] = useState(null);
const [filteredData, setData] = useState(props.cardsToShow);
const handleSort = clickedColumn => {
if (column !== clickedColumn) {
setColumn(clickedColumn);
setData(_.sortBy(filteredData, [clickedColumn]));
setDirection("ascending");
return;
}
setData(_.sortBy(filteredData.reverse()));
direction === "ascending"
? setDirection("descending")
: setDirection("ascending");
};
return (
<>
<div>
<Search />
<h3>Vintage Card Search</h3>
<Table sortable celled fixed striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell
sorted={column === "title" ? direction : null}
onClick={handleSort("title")}
>
Card Title
</Table.HeaderCell>
<Table.HeaderCell># Bids</Table.HeaderCell>
<Table.HeaderCell>Watchers</Table.HeaderCell>
<Table.HeaderCell>Price</Table.HeaderCell>
<Table.HeaderCell>Time Left</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{props.cardsToShow.map(card => (
<>
<Table.Row key={card.id}>
<Table.Cell>{card.title}</Table.Cell>
<Table.Cell>
{card.sellingStatus[0].bidCount
? card.sellingStatus[0].bidCount
: 0}
</Table.Cell>
<Table.Cell>
{card.listingInfo[0].watchCount
? card.listingInfo[0].watchCount
: 0}
</Table.Cell>
<Table.Cell>
$
{card.sellingStatus &&
card.sellingStatus[0].currentPrice[0]["__value__"]}
</Table.Cell>
<Table.Cell>
<TimeAgo
date={new Date(
card.listingInfo && card.listingInfo[0].endTime
).toLocaleDateString()}
/>
</Table.Cell>
</Table.Row>
</>
))}
</Table.Body>
</Table>
</div>
</>
);
};
const mapStateToProps = state => {
return {
baseball: state.baseball,
search: state.search,
cardsToShow: searchCards(state)
};
};
const mapDispatchToProps = {
searchChange
};
export default connect(mapStateToProps, mapDispatchToProps)(Cards);
// export default Cards;
Yachaka has already pointed out the incorrect line, but their answer doesn't explain what the issue is.
When you pass props in React with prop={expression}, the expression in the brackets gets evaluated, much like function arguments are evaluated when they are passed. Hence, whenever the component is rendered, handleSort("title") is called. This function then causes the props to be updated, and the component is re-rendered, causing the cycle to repeat indefinitely.
So the problem is that, instead of passing a function that should be called when the button is clicked, you call that function (with handleSort("title")), which results in undefined, and causes a feedback loop.
Instead you should use an expression that returns a function. The most concise way of doing that in JavaScript is an arrow function, as Yachaka mentioned () => handleSort("title"). This evaluates to a function that calls handleSort.
Change this line:
onClick={handleSort("title")}
by
onClick={() => handleSort("title")}
EDIT: Reinis has written a nice explanation below!
Related
I'm trying to get rid of a warning message in the project I'm working on.
index.js:1 Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-find-node
at div
at Transition (http://localhost:3000/static/js/vendors~main.chunk.js:47483:30)
at CSSTransition (http://localhost:3000/static/js/vendors~main.chunk.js:46600:35)
at div
at TransitionGroup (http://localhost:3000/static/js/vendors~main.chunk.js:48052:30)
at Contacts (http://localhost:3000/static/js/main.chunk.js:1623:96)
at div
at div
at Home (http://localhost:3000/static/js/main.chunk.js:2549:88)
at AuthCheck (http://localhost:3000/static/js/main.chunk.js:2705:5)
at Routes (http://localhost:3000/static/js/vendors~main.chunk.js:45749:5)
at div
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:45682:15)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:45198:5)
at ContactState (http://localhost:3000/static/js/main.chunk.js:3743:85)
at AuthState (http://localhost:3000/static/js/main.chunk.js:3243:85)
at AlertState (http://localhost:3000/static/js/main.chunk.js:2844:85)
at App
The problematic code:
import React, { Fragment, useEffect } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { useContactContext } from '../../context/contact/contactContext';
import { useAuthtContext } from '../../context/auth/authContext';
import ContactItem from './ContactItem';
import Spinner from '../layout/Spinner';
const Contacts = () => {
const { contacts, filtered, getContacts, loading } = useContactContext();
const { isAuthenticated } = useAuthtContext();
useEffect(() => {
if (isAuthenticated) {
getContacts();
}
// eslint-disable-next-line
}, [isAuthenticated]);
if (!loading && contacts !== null && contacts.length === 0) {
return <h4>Please add a contact</h4>;
}
return (
<Fragment>
{contacts === null || loading ? (
<Spinner />
) : (
<TransitionGroup>
{(filtered || contacts).map((contact) => (
<CSSTransition timeout={1000} classNames="item" key={contact._id}>
<ContactItem contact={contact} />
</CSSTransition>
))}
</TransitionGroup>
)}
</Fragment>
);
};
export default Contacts;
I've spent a few hours looking for answers, but I feel like I'm running around in an endless loop.
To get rid of the warning, I need to use useRef hooks on each CSSTransition element, to connect it with (it's children?).
I can't use useRef() inside the render function of a component, so I defined a new component to display each TransitionItem:
...
const TransitionItem = ({ contact, ...props }) => {
const ref = useRef(null); // Had to use this ref to go around a warning
return (
<CSSTransition nodeRef={ref} timeout={1000} classNames="item" {...props}>
<div ref={ref}>
<ContactItem contact={contact} />
</div>
</CSSTransition>
);
};
return (
<Fragment>
{contacts === null || loading ? (
<Spinner />
) : (
<TransitionGroup>
{(filtered || contacts).map((contact) => (
<TransitionItem key={contact._id} contact={contact} />
))}
</TransitionGroup>
)}
</Fragment>
);
...
Now every time I try to click on a button, to remove an item from the list, I see a "flashing" effect, you can check out in this Sandbox: (Click on the red buttons to remove an item)
https://codesandbox.io/s/kind-feather-2psuz
The "flashing" problem only starts when I move the CSSTransition component into the new TransitionItem component, but I can't use useRef hooks on each item if I don't move it there.
Help pls! :)
PS:
Removing <React.StrictMode> from the index.js is not a solution to the root problem.
I have the same warning in my project and i can fix it with this solution, thank pixel-fixer !
Issue #668 on repo react-transition-group
From 4.4.0 release notes:
react-transition-group internally uses findDOMNode, which is
deprecated and produces warnings in Strict Mode, so now you can
optionally pass nodeRef to Transition and CSSTransition, it's a ref
object that should point to the transitioning child:
You can fix this like this
import React from "react"
import { CSSTransition } from "react-transition-group"
const MyComponent = () => {
const nodeRef = React.useRef(null)
return (
<CSSTransition nodeRef={nodeRef} in timeout={200} classNames="fade">
<div ref={nodeRef}>Fade</div>
</CSSTransition>
)
}
I hope it works for you, have a nice day !
My api data is being successfully passed from my api call into the table component but is not being rendered.
If after searching for a playlist I go in and make an edit to the table.js file the data will render correctly.
App.js...
const App = (props) => {
const [playlists, setPlaylists] = useState([])
const [searchString, setSearchString] = useState() //use instead of onsubmit
const isFirstRef = useRef(true);
const search = (value) => {
setSearchString(value)
}
useEffect(
() => {
if (isFirstRef.current) {
isFirstRef.current = false;
return;
}
let spotlist = Spotify.playlistsearch(searchString)
let tablelist = []
spotlist.then(val =>{
val.forEach(element =>{
tablelist.push(
{
name: element.description,
track_count: element.tracks.total,
})
}
)})
setPlaylists(tablelist)
}, [searchString] );
return (
<div className="App">
<Searchform search={search}/>
<Table playlists={playlists}/>
</div>
)
};
The playlists prop is being shown as present under the PlayListTable component inspector but is not rendering. I am able to get the data to render IF I edit the file after seeing the data present in the component inspector.
Table.js
import React from 'react'
import { Icon, Label, Menu, Table } from 'semantic-ui-react'
const PlayListTable = ({ playlists }) => {
return(
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Playlist</Table.HeaderCell>
<Table.HeaderCell>Track Count</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{playlists.map( (playlist) => {
return (
<Table.Row>
<Table.Cell>
{playlist.name}
</Table.Cell>
<Table.Cell>
{playlist.track_count}
</Table.Cell>
</Table.Row>
)
}
)
}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan='3'>
<Menu floated='right' pagination>
<Menu.Item as='a' icon>
<Icon name='chevron left' />
</Menu.Item>
<Menu.Item as='a'>1</Menu.Item>
<Menu.Item as='a'>2</Menu.Item>
<Menu.Item as='a'>3</Menu.Item>
<Menu.Item as='a'>4</Menu.Item>
<Menu.Item as='a' icon>
<Icon name='chevron right' />
</Menu.Item>
</Menu>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
)
}
export default PlayListTable
Issue
You may be receiving correct data but you don't update your state at the correct time. spotlist returns a Promise that you are chaining from, but the setPlaylists(tablelist) call is enqueued before the then block of the promise chain is processed. The useEffect callback is 100% synchronous code.
let spotlist = Spotify.playlistsearch(searchString);
let tablelist = [];
spotlist.then(val => { // <-- (1) then callback is placed in the event queue
val.forEach(element => { // <-- (3) callback is processed, tablelist updated
tablelist.push({
name: element.description,
track_count: element.tracks.total,
});
}
)});
setPlaylists(tablelist); // <-- (2) state update enqueued, tablelist = []
Solution - Place state update in Promise chain
You can forEach into a temp array, but mapping the response data to state values is the more "React" way of handling it. It is also more succinct.
Spotify.playlistsearch(searchString)
.then(val => {
setPlaylists(val.map(element => ({
name: element.description,
track_count: element.tracks.total,
})));
}
)});
The first note I’d have is that your use effect will not trigger unless something changes with searchString since it’s in the dependency array. Additionally, I believe that your first ref condition prevents the first render from setting up, so it could be causing issues.
It may be better to look into something like a previous check, I’ve used the solution for a common hook from here before: How to compare oldValues and newValues on React Hooks useEffect?
When you say the playlist prop is showing up, are there actual values?
On other edit make sure the return value from map has proper keys: https://reactjs.org/docs/lists-and-keys.html
I have the following component:
import React, { useState } from "react";
import { FormControl, TextField } from "#material-ui/core";
interface IProps {
text?: string;
id: number;
onValueChange: (text: string, id: number) => void;
placeholder: string;
}
export const QuestionTextRow: React.FC<IProps> = (props) => {
const [item, onItemChange] = useState(props.text);
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
const newValue = e.currentTarget.value;
onItemChange(newValue);
props.onValueChange(newValue, props.id);
};
return (
<>
<FormControl fullWidth>
<div>{props.text}</div>
<TextField
aria-label="question-text-row"
onDragStart={(e) => {
e.preventDefault();
e.stopPropagation();
}}
value={item}
onChange={(ev: React.ChangeEvent<HTMLInputElement>): void => {
onChange(ev);
}}
/>
</FormControl>
</>
);
};
It is rendered via the following component:
const renderQuestionOptions = (id: number): JSX.Element => {
const item = props.bases.find((x) => x.sortableId === id);
if (!item) return <> </>;
return (
<div className={classes.questionPremiseRow}>
<div className={classes.rowOutline}>
<QuestionOptionsSortableRow item={item} isDisabled={false} onClickRow={onClickBasisRow}>
<QuestionTextRow
text={item.text ? item.text.text : ""}
id={item.sortableId}
onValueChange={basisValueChanged}
placeholder={intl.formatMessage({ id: "question.create.basis.row.placeholder" })}
></QuestionTextRow>
</QuestionOptionsSortableRow>
</div>
</div>
);
};
It renders the following list:
As you can see props.text and useState item from props.text are rendered equally. If props.text is updated it does not reflect on useState though.
https://stackoverflow.com/a/53846698/3850405
I can solve it by useEffect to make it work:
useEffect(() => {
onItemChange(props.text);
}, [props.text]);
https://reactjs.org/docs/hooks-effect.html
https://stackoverflow.com/a/54866051/3850405
However If I add key={`${item.text?.text}-${item.sortableId}`} to QuestionTextRow it will work without using useEffect. How come?
I know a static unique key should be used but would it not be the same result if key={item.uniqueId} was used?
https://www.npmjs.com/package/react-key-index
The argument passed to useState is the initial state much like setting
state in constructor for a class component and isn't used to update
the state on re-render
https://stackoverflow.com/a/43892905/3850405
However If I add key={${item.text?.text}-${item.sortableId}} to
QuestionTextRow it will work without using useEffect. How come?
That is because of reconciliation. In react, when on one render you have say:
<SomeComponent key={1}/>
If on next render you render same component (at the same place) with different key, say:
<SomeComponent key={2}/>
React will destroy instance related to previous component and create a new instance for this one, hence the useState inside that component will be initialized with the provided text property once again (like when you created the component first time).
If the key is same for some component on previous and next renders and you just change some other props, in this case the component is re-rendered (no instance destroyed), that's why you didn't see the text property reflected in state.
Sometimes it can be tricky to copy props to state like you have in your useEffect solution, I recommend you read this post, it is about classes but same ideas apply.
I'm trying to make this table sortable but in the example are using class component and i have a functional component.
I try to make this way:
const WorkOutList = props => {
const handleSort = (clickedColumn) => () => {
const [column, data, direction] = useState(0);
if (column !== clickedColumn) {
this.setState({
column: clickedColumn,
data: _.sortBy(data, [clickedColumn]),
direction: 'ascending',
})
return
}
this.setState({
data: data.reverse(),
direction: direction === 'ascending' ? 'descending' : 'ascending',
})
}
const renderRows = () => {
const list = props.list || []
return list.map(workout => (
<Table.Row key={workout.id}>
<Table.Cell>{workout.tempoGasto}h</Table.Cell>
<Table.Cell>{workout.tipoTarefa}</Table.Cell>
<Table.Cell>{workout.data}</Table.Cell>
<Table.Cell>
<Button
animated='vertical'
onClick={() => props.removeWorkout(workout)}
>
<Button.Content hidden>Deletar</Button.Content>
<Button.Content visible>
<Icon name='trash' />
</Button.Content>
</Button>
</Table.Cell>
</Table.Row>
))
}
return (
<Table sortable celled fixed>
<Table.Header>
<Table.Row>
<Table.HeaderCell
sorted={props.column === 'tempoGasto' ? direction : null}
onClick={handleSort('tempoGasto')}
>
Tempo
</Table.HeaderCell>
<Table.HeaderCell
sorted={props.column === 'tipoTarefa' ? direction : null}
onClick={handleSort('tipoTarefa')}
>
Tipo
</Table.HeaderCell>
<Table.HeaderCell
sorted={props.column === 'data' ? direction : null}
onClick={handleSort('data')}
>
Data
</Table.HeaderCell>
<Table.HeaderCell>
Ações
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{renderRows()}
</Table.Body>
</Table>
)
}
I'm using react and redux, but i'm receiving:
Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
The full code is in pastebin
This is because you have used useState hook inside of a function.
const [column, data, direction] = useState(0);
You need to write this at your component level outside of any function.
Another issue is, you have incorrect definition of useState. useState give you pair of the value and it's setter function.
const [data, setData] = useState(0)
Now you can change value of data using setData setter function,
setData(1)
Read more about useState here.
You can actually store object in state,
const [data, setData] = useState({
column: "",
data: "",
direction: "",
})
And use setData setter function for state change,
setData(prevState => ({
...prevState,
column: clickedColumn,
data: _.sortBy(prevState.data, [clickedColumn]),
direction: 'ascending',
})
)
Check this for how to use useState.
I have milestoneCards.
I want to add a sort button, that upon clicking this button the cards will be sorted by the card heading.
The sort takes place, but it does not re-render the list in the sorted order.
please advise.
thank you so much for helping me here.
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { Card, CardBody, CardTitle } from "reactstrap";
const MyMilestones = props => {
let sortClicked = false;
let milestoneCards =
props.milestones.length > 0
? props.milestones.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
))
: null;
const sortedMilestoneCards = [...props.milestones]
.sort((a, b) => (a.attributes.heading > b.attributes.heading ? 1 : -1))
.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
));
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
sortClicked = true;
console.log("before", milestoneCards);
milestoneCards = sortedMilestoneCards;
console.log("after", milestoneCards);
return (milestoneCards = sortedMilestoneCards);
}}
>
Sort
</button>
sortClicked ? ({sortedMilestoneCards}) : {milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
const mapStateToProps = state => {
return {
milestones: state.myMilestones
};
};
export default connect(mapStateToProps)(MyMilestones);
It's because you need to have sortClicked to be tracked by React.
When let sortClicked = false is declared inside MyMilestones component, it's declared once on the first component mount and won't be updated when the component is re-rendered.
So you can save sortClicked in a state using React.useState and update it onClick. useState is a one-off way of storing this.state value for Class Component but for one state. (I won't get into it too deep as React documentation has a thorough coverage on Introducing Hooks)
const MyMilestones = props => {
// let sortClicked = false;
// Initialize it to "false" by default.
let [sortClicked, setSortClicked] = React.useState(false)
let milestoneCards = ...;
const sortedMilestoneCards = ...;
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
// Notify "React" to re-render.
setSortClicked(true)
// No need to return a new reference here.
}}
>
Sort
</button>
{/* 👇 Note that {} is wrapped around the whole block. */}
{sortClicked ? sortedMilestoneCards : milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
It's because you're not updating the milestones correctly. Since they're stored on Redux state, you need to add and dispatch the action that modifies the state.
I recommend you look at the Redux documentation.