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'/>}
</>
)};
Related
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.
I'm not sure the title is correct, so let me try to explain what I'm trying to achieve.
Let's say I have a flow in my application that has 3 steps in it, so I create a component (let's call it Stepper) with 3 child components where each child is a component that renders the corresponding step.
I want to expose a custom hook to the child components of Stepper, let's call it useStepper.
This is how Stepper would look like (JSX-wise):
export const Stepper = (props) => {
...some logic
return (
<SomeWrapper>
{props.children}
</SomeWrapper>
);
};
so I can make components like this:
export SomeFlow = () => {
return (
<Stepper>
<StepOne />
<StepTwo />
<StepThree />
</Stepper>
);
};
Now this is how I want things to work inside Stepper's children, let's take StepThree as an example:
export const StepThree = () => {
const exposedStepperData = useStepper();
... some logic
return (
...
);
};
Now, it's important that the Stepper will be reusable; That means - each Stepper instance should have its own data/state/context that is exposed through the useStepper hook.
Different Stepper instances should have different exposed data.
Is it possible to achieve this? I tried to use Context API but I was not successful. It's also weird that I couldn't find anything about it on the internet, maybe I searched wrong queries as I don't know what patten it is (if it exists).
Note:
I achieved a similar behavior through injected props from parent to its children, but it's not as clean as I want it to be, especially with Typescript.
I recently came across something like this, it was solved by pouring all the components/steps in an array and let the hook manage which component/step to show. If you want it to be more reusable you could pass in the children to the array.
I hope this helps you in the right direction
useStepper.ts
import { ReactElement, useState } from "react";
export const useStepper = (steps: ReactElement[]) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const next = () => {
setCurrentStepIndex((i: number) => {
if (i >= steps.length - 1) return i;
return i + 1;
});
};
const back = () => {
setCurrentStepIndex((i: number) => {
if (i <= 0) return i;
return i - 1;
});
};
const goTo = (index: number) => {
setCurrentStepIndex(index);
};
return {
currentStepIndex,
step: steps[currentStepIndex],
steps,
isFirstStep: currentStepIndex === 0,
isLastStep: currentStepIndex === steps.length - 1,
goTo,
next,
back,
};
};
Stepper.tsx
// const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
// useStepper([<StepOne />, <StepTwo />, <StepThree />]);
const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
useStepper([...children]);
return (
<div>
{!isFirstStep && <button onClick={back}>Back</button>}
{step}
<button onClick={next}>{isLastStep ? "Finish" : "Next"}</button>
</div>
);
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>
)
}
I have a list of "selections" that are displayed using a component. I need to find the rendered width of all these selections. My template looks like this:
{props.selections.map((chip: SelectOptionType) => {
return (
<Chip text={chip.label} />
)
}
Typically, in a non-React application, I'd probably put a class on the <Chip /> and use jquery to select elements of that class name, then loop over them and just sum the widths together:
let sum: number = 0;
$(".someClassName").forEach(($el) => sum += $el.offsetWidth);
I know the suggested way of doing something similar to this is using refs, but it seems you cant create an array of refs. I tried doing something like this:
{props.selections.map((chip: SelectOptionType, index: number) => {
chipsRefs[index] = React.createRef<HTMLDivElement>();
return (
<div ref={chipsRefs[index]}>
<Chip text={chip.label} />
</div>
)
}
But as I quickly learned each Ref inside chipsRefs ended up with a null current.
Now I'm a bit at a loss for this and have tried finding examples of this use case but have come up empty.
Can you try this ?
ref={ref => {
chipsRefs[index] = ref
}}
Try doing something like this: https://codesandbox.io/s/awesome-haibt-zeb8m
import React from "react";
class Selections extends React.Component {
constructor(props) {
super(props);
this._nodes = new Map();
}
componentDidMount() {
this.checkNodes();
}
checkNodes = () => {
let totalWidth = 0;
Array.from(this._nodes.values())
.filter(node => node != null)
.forEach(node => {
totalWidth = totalWidth + node.offsetWidth;
});
console.log(totalWidth);
};
render() {
const { selections } = this.props;
return (
<div>
{selections.map((value, i) => (
<div key={i} ref={c => this._nodes.set(i, c)}>
{value}
</div>
))}
</div>
);
}
}
export default Selections;
The function we defined in the ref prop is executed at time of
render.
In the ref call-back function, ref={c => this._nodes.set(i, c)}
we pass in the index (i) provided by .map() and the html element
(c) that is provided by the ref prop, in this case the div itself.
this._nodes.set(i, c) will create a new key-value pair in our
this._nodes iterable, one pair for each div we created. Now we have recorded HTML elements (nodes) to work with that contain all the methods we need to calculate the totalWidth of your rendered list.
Lastly in checkNodes() we get the .offsetWidth of each node to get our totalWidth.