I've been writing a chess application in order to help myself get up to speed on hooks introduced in React. I've got two main components so far; one for the board itself and one for a move history that allows you to revert back to a previous move. When I try to use a callback in the Board component to pass a move to the move history, I get an error Cannot update a component ('App') while rendering a different component ('MoveHistory'). I understand the error but I'm not fully sure what I'm supposed to do about it.
My components (minus all the parts I'm pretty sure are irrelevant) are as follows:
App.tsx (parent component)
...
const STARTING_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
function App() {
const [FEN, setFEN] = useState(STARTING_FEN);
const [moveHistory, setMoveHistory] = useState<string[]>([]);
const [fenHistory, setFenHistory] = useState<string[]>([]);
// rewind game state to specified move index
function onRewind(target: number) {
setFEN(fenHistory[target]);
setMoveHistory(moveHistory.slice(0, target));
setFenHistory(fenHistory.slice(0, target));
}
// add a new move to the move history
function onMove(move: string, FEN: string) {
setMoveHistory([...moveHistory, move]);
setFenHistory([...fenHistory, FEN]);
}
return (
<div className='app'>
<Board FEN={FEN} onMove={onMove} />
<MoveHistory moves={moveHistory} onRewind={onRewind} />
</div>
);
}
export default App;
Board.tsx (sibling component 1)
...
interface BoardProps {
FEN: string;
onMove: Function;
}
function Board(props: BoardProps) {
const splitFEN = props.FEN.split(' ');
const [squares, setSquares] = useState(generateSquares(splitFEN[0]));
const [lastClickedIndex, setLastClickedIndex] = useState(-1);
const [activeColor, setActiveColor] = useState(getActiveColor(splitFEN[1]));
const [castleRights, setCastleRights] = useState(getCastleRights(splitFEN[2]));
const [enPassantTarget, setEnPassantTarget] = useState(getEnPassantTarget(splitFEN[3]));
const [halfMoves, setHalfMoves] = useState(parseInt(splitFEN[4]));
const [fullMoves, setFullMoves] = useState(parseInt(splitFEN[5]));
...
// handle piece movement (called when a user clicks on a square)
function onSquareClicked(index: number) {
... // logic determining where to move the piece
{
props.onMove(moveName, getFEN())
}
}
...
// get the FEN string for the current board
function getFEN(): string {
... //logic converting board state to strings
return `${pieceString} ${activeString} ${castleString} ${enPassantString} ${halfMoves} ${fullMoves}`;
}
return (
<div className='board'>
{[...Array(BOARD_SIZE)].map((e, rank) => {
return (
<div key={rank} className='row'>
{squares.slice(rank * BOARD_SIZE, BOARD_SIZE + rank * BOARD_SIZE).map((square, file) => {
return (
<Square
key={file}
index={coordsToIndex(rank, file)}
pieceColor={square.pieceColor}
pieceType={square.pieceType}
style={square.style}
onClick={onSquareClicked}
/>
);
})}
</div>
)
})};
</div>
);
}
export default Board;
MoveHistory.tsx (sibling component #2)
...
interface MoveHistoryProps {
moves: string[],
onRewind: Function;
}
function MoveHistory(props: MoveHistoryProps) {
return (
<div className='move-history'>
<div className='header'>
Moves
</div>
<div className='move-list'>
{_.chunk(props.moves, 2).map((movePair: string[], index: number) => {
return (
<div className='move-pair' key={index}>
<span>{`${index + 1}.`}</span>
<span onClick={props.onRewind(index * 2)}>{movePair[0]}</span>
<span onClick={props.onRewind(index * 2 + 1)}>{movePair[1] || ""}</span>
</div>
)
})}
</div>
</div>
)
}
export default MoveHistory;
I've looked at a bunch of other stackoverflow questions that seem to answer the question I'm asking here but to me it looks like I'm already doing what's recommended there, so I'm not sure what the difference is. I've also seen some recommendations to use Redux for this, which I'm not opposed to, but if it can be avoided that would be nice.
The issue is that you are calling props.onRewind in the render of your MoveHistory. This is effectively what happens:
App starts rendering
MoveHistory starts rendering, and calls onRewind
Within onRewind, you call various useState setter methods within App. App hasn't finished rendering yet, but it's state-modifying methods are being called. This is what triggers the error.
I think you mean to do something like this instead:
...
interface MoveHistoryProps {
moves: string[],
onRewind: Function;
}
function MoveHistory(props: MoveHistoryProps) {
return (
<div className='move-history'>
<div className='header'>
Moves
</div>
<div className='move-list'>
{_.chunk(props.moves, 2).map((movePair: string[], index: number) => {
return (
<div className='move-pair' key={index}>
<span>{`${index + 1}.`}</span>
<span onClick={() => props.onRewind(index * 2)}>{movePair[0]}</span>
<span onClick={() => props.onRewind(index * 2 + 1)}>{movePair[1] || ""}</span>
</div>
)
})}
</div>
</div>
)
}
export default MoveHistory;
Note that, instead of calling props.onRewind you are giving it a method which, when the span is clicked, will call onRewind.
Related
In a react component I have an array of things. I iterate through that array display the name of the thing in a plain div, then pass each element to another component to display details.
What's happening: if I delete an element from anywhere except the bottom (last array element) the header that is displayed in the main element containing the array is correct (the one I clicked "delete" on disappeared), but the "body" (which is another component) remains. Instead, the inner component is acting as if I deleted the last element of the array and kind of "moves" up the array.
It's hard to describe in words. See example below. Delete the top element or one of the middle ones and see how the header for the section starts not matching the contents.
I'm trying to understand why this is happening.
(EDIT/NOTE: State IS needed in the child component because in real life it's a form and updates the object being passed in. I Just removed the updating here to make the example shorter and simpler)
Example code (delete the middle element of the array and see what happens):
https://codesandbox.io/s/confident-buck-dodvgu?file=/src/App.tsx
Main component:
import { useState, useEffect } from "react";
import InnerComponent from "./InnerComponent";
import Thing from "./Thing";
import "./styles.css";
export default function App() {
const [things, setThings] = useState<Thing[]>([]);
useEffect(() => resetThings(), []);
const resetThings = () => {
setThings([
{ name: "dog", num: 5 },
{ name: "cat", num: 7 },
{ name: "apple", num: 11 },
{ name: "book", num: 1}
]);
};
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
return (
<div className="App">
{things.map((thing, index) => (
<div key={`${index}`} className="thing-container">
<h2>{thing.name}</h2>
<InnerComponent
thing={thing}
index={index}
onDelete={onDeleteThing}
/>
</div>
))}
<div>
<button onClick={resetThings}>Reset Things</button>
</div>
</div>
);
}
Inner component:
import { useEffect, useState } from "react";
import Thing from "./Thing";
interface InnerComponentParams {
thing: Thing;
index: number;
onDelete: (indexToDelete: number) => void;
}
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
const [name, setName] = useState(thing.name);
const [num, setNum] = useState(thing.num);
return (
<div>
<div>Name: {name}</div>
<div>Num: {num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
export default InnerComponent;
You are creating unnecessary states in the child component, which is causing problems when React reconciles the rearranged Things. Because you aren't setting the state in the child component, leave it off entirely - instead, just reference the prop.
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
return (
<div>
<div>Name: {thing.name}</div>
<div>Num: {thing.num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
The other reason this is happening is because your key is wrong here:
{things.map((thing, index) => (
<div key={`${index}`}
Here, you're telling React that when an element of index i is rendered, on future renders, when another element with the same i key is returned, that corresponds to the JSX element from the prior render - which is incorrect, because the indicies do not stay the same. Use a proper key instead, something unique to each object being iterated over - such as the name.
<div key={thing.name}
Using either of these approaches will fix the issue (but it'd be good to use both anyway).
This is also wrong. You're removing everything except the index.
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
Use filter:
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things].filter(
(thing, index) => index !== indexToDelete
);
setThings(newThings);
};
I am creating a React app with a Django backend and using Redux to maintain state. I am trying to render data in a child component and I keep getting this error: Objects are not valid as a React child (found: object with keys {id, sets, reps, weight, bridge_id}). If you meant to render a collection of children, use an array instead.
What I have is a main screen RoutineScreen.js which is supposed to render child components displaying the exercises in the routine and sets, reps, weight for each.
This is what my code looks like:
function RoutineScreen() {
// Get routine id which is passed in through program screen components
const { id } = useParams()
const routine_id = id
//Extract program id from routine
const state = {...store.getState()}
let program_id
// -- set routines, which we will loop through to find program id
const routines = state.programRoutines.routines
// -- loop through routines, find match for routine_id
for (let i in routines) {
if (i.id == routine_id){
program_id = i.program
}
}
const dispatch = useDispatch()
const routineExercises = useSelector(state => state.routineExercises)
const { error, loading, exercises } = routineExercises
useEffect(() => {
dispatch(listRoutineExercises(program_id, routine_id))
}, [dispatch])
return (
<div className="screen-container">
<Header/>
<SearchBar/>
<div className="card-container">
{exercises.map((exercise, index) => (
// Render Routines
<Exercise key={exercise.id} routine_id={routine_id} index={index} exercise={exercise}/>
))}
</div>
</div>
)
}
export default RoutineScreen
function Exercise({ exercise, routine_id, index }) {
// Get routine id which was passed in through program screen components
const { id } = useParams()
const exercise_id = id
const dispatch = useDispatch()
// use spread operator to unpack elements from exerciseParameters
const exerciseParameters = useSelector(state => state.exerciseParameters)
const { error, loading, parameters } = exerciseParameters
useEffect(() => {
dispatch(listExerciseParams(routine_id, exercise_id))
}, [dispatch])
return (
<div style={{ textDecoration: 'none' }}>
<div>
<h3>{exercise.name} </h3>
<h4>{parameters[index].reps}</h4>
<h4>{parameters[index].weight}</h4>
</div>
</div>
)
}
export default Exercise
Here is my data:
Note: I am completely new to React and Redux so feel free to let me know if there are also any other suggestions for how I should fix my code or if I am missing any relevant information for this problem. Thanks!
I tried to access the specific elements in the object by using parameters[index].reps, expecting that it would then display that data, but instead I received the error above.
I think you can do something like :
return(
<div style={{ textDecoration: 'none' }}>
<div>
<h3>{exercise.name} </h3>
{parameters.map(parameter => (
<>
<h4>{parameter.reps}</h4>
<h4>{parameter.weight}</h4>
</>
))}
</div>
</div>
)
I'm trying to create a helper function that nest a passed element/component as a children of itself of several depths.
So something like this
const Chain = nested(4, () => <div />)
return (
<Chain />
)
Should render divs nested 4 levels
<div>
<div>
<div>
<div />
</div>
</div>
</div>
I implemented the function in React like this.
/** nest the given element in itself of specified depth */
export const nested = (
depth: number,
element: () => JSX.Element
): FC<any> => () => {
let chain = element();
for (let i = 0; i < depth; i++) {
chain = React.createElement(
element().type,
element().props,
chain
);
}
return chain;
};
It renders and displays the nested Divs correctly, but gives back an error message i don't understand how to correct.
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
Try going with a recursive solution.
const createInnerElements = (count, childElement) => {
if (count == 1) {
return <div>{childElement}</div>
}
return createInnerElements(count - 1, <div>{childElement}</div>);
}
// in your parent component,
return <>{createInnerElements(4, <></>)}</>
Here's the solution I settled with. Credit to #senthil balaji's answer.
export const NestedTags = (depth: number, childElement: JSX.Element): JSX.Element => {
if (depth === 1) {
return (
<childElement.type {...childElement.props}>
{childElement}
</childElement.type>
);
}
return NestedTags(
depth - 1,
<childElement.type {...childElement.props}>
{childElement}
</childElement.type>
);
};
and should be wrapped in a React Fragment using used in parent.
<>
{NestedTags(5, <div className='segment'><div>)}
</>
The caveat for this is that it can't nest when passed a React Component Type like so:
const Test = ({className}: {className: string}) => (
<a className={className}>hello</a>;
)
//...
return (
<>
{NestedTags(5, <Test className='test'/>}
</>
)};
I am having an issue where I have an element, but I cannot seem to reference it without getting a null error.
render() {
const getXandY = () => {
const elment = document.getElementById("test");
const xpos = elment.style.left;
const ypos = elment.style.top;
console.log(xpos);
console.log(ypos);
};
return (
getXandY(),
(
<div id="test" className="treeContainer">
{this.state.configs.map((config) => (
<div>
<Configs configName={config.name} configNumber={config.id} />
</div>
))}
</div>
)
);
}
}
any help is appreciated.
I'm not sue you understand React. In render() function try to avoid defining functions, it's a function for just returning the JSX you want to render (you can't return nothing else there). So define your getXandY outside render(). If you really want to invoke it on rendering, simply put the function call inside, that's it. So it would look something like:
const getXandY = ...
render() {
getXandY();
return (
<div ... />
);
{
In my application I have a list of "chips" (per material-ui), and on clicking the delete button a delete action should be taken. The action needs to be given a reference to the chip not the button.
A naive (and wrong) implementation would look like:
function MemberList(props) {
const {userList} = this.props;
refs = {}
for (const usr.id of userList) {
refs[usr.id] = React.useRef();
}
return <>
<div >
{
userList.map(usr => {
return <UserThumbView
ref={refs[usr.id]}
key={usr.id}
user={usr}
handleDelete={(e) => {
onRemove(usr, refs[usr.id])
}}
/>
}) :
}
</div>
</>
}
However as said this is wrong, since react expects all hooks to always in the same order, and (hence) always be of the same amount. (above would actually work, until we add a state/any other hook below the for loop).
How would this be solved? Or is this the limit of functional components?
Refs are just a way to save a reference between renders. Just remember to check if it is defined before you use it. See the example code below.
function MemberList(props) {
const refs = React.useRef({});
return (
<div>
{props.userList.map(user => (
<UserThumbView
handleDelete={(e) => onRemove(user, refs[user.id])}
ref={el => refs.current[user.id] = el}
key={user.id}
user={user}
/>
})}
</div>
)
}