Passing array as props changes type - reactjs

I have the following;
export default function World() {
const { backgroundTiles } = typedLevelData[`level_1`];
console.log(Array.isArray(backgroundTiles));
return (
<Background {...backgroundTiles}/>
)
}
As I'd expect, the console log returns true. However, in the Background component, attempting to call forEach returns a TypeError, stating that background.forEach is not a function.
export default function Background(backgroundTiles : BackgroundTile[]) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current!;
const ctx2d = canvas.getContext('2d')!;
ctx2d?.clearRect(0, 0, 1200, 600);
console.log(Array.isArray(backgroundTiles));
backgroundTiles.forEach(async ({tileType, positions}) => {
const image = await getImageByTileType[tileType];
positions.forEach(([x, y]) => {
ctx2d.drawImage(image, x, y);
})
})
}, [backgroundTiles])
return (
<canvas
className={styles.canvas}
ref={canvasRef}
height={600}
width={1200}
/>
)
}
Edit: BackgroundTile:
export interface BackgroundTile {
tileType: TileTypes,
passable: boolean,
positions: number[][]
}

You are trying to spread an array into an object.
It just iterates the array as if it was an object, and adds properties on the fly, here is an example
const arr = ['a','b','c']
{...arr} === {0: "a", 1: "b", 2: "c"} // something similar goes to your component.
So the solution is - just pass a named prop to Background (this is preferable option)
<Background backgroundTiles={backgroundTiles}/>
or destruct the whole object with backgroundTiles
<Background {...typedLevelData[`level_1`]}/>
But also don't forget to destruct it in the Background Component,
export default function Background({backgroundTiles} : {backgroundTiles:BackgroundTile[]}) {
because first argument that a component receives (props) is an object

Not sure how backgroundTiles looks like, but you better try passing it like that to your component:
export default function World() {
const { backgroundTiles } = typedLevelData[`level_1`];
console.log(Array.isArray(backgroundTiles));
return (
<Background backgroundTiles={backgroundTiles}/>
)
}

Related

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

React typescript recognizes array as object

new to react and js in general so it might be a dumb question but I can't sort it out.
I have a component like so:
export interface TableViewContainerProps {
header: WorthSummaryContainerProps
content: TableViewRowProps[]
}
export const TableViewContainer = (props: TableViewContainerProps) => {
console.log('content type is ', typeof(props.content))
console.log('content is ', props.content)
return (
<div id="tableview-container">
<div id="tableview">
<TableViewHeader {...props.header}/>
<TableViewContentList {...props.content} />
<div id="tableview-footer">
</div>
</div>
</div>
)
}
So when I print it it's an array of objects, all good.
TableViewContentList gets the content as props:
export const TableViewContentList = (props: TableViewRowProps[]) => {
console.log('type of TableViewContentList props is: ', typeof(props), props)
const tableViewContents = props.map((row) => console.log('row'))
return (
<div id="tableview-content-list">
{tableViewContents}
</div>
)
}
so when I print it here it's an object not an array anymore and it breaks at the .map. Can someone help me out please? I feel like I'm missing something minor.
Spread syntax (...) will give you an object when you apply it on an array inside an object.
type TableViewRowProps = number;
interface TableViewContainerProps {
content: TableViewRowProps[]
}
const props = {content: [1,2,3]}
const b = {...props.content}
console.log(b) // { "0": 1, "1": 2, "2": 3 }.
So the TableViewContentList get the props: props[0], props[1] and props[2], it's wrong.
All properties passed into a component will be attached in props, that's why you got an object. props will always be an object. So you'd better pass the content like this:
<TableViewContentList {...props} />
Or this:
<TableViewContentList content={props.content} />
Then you can map it to row:
export default function TableViewContentList(props: { content: TableViewRowProps[] }) {
const tableViewContents = props.content.map((row) => console.log('row'));
return <div id="tableview-content-list">{tableViewContents}</div>;
}

Why calling useState update function through other function passed as prop to child component won't work?

This is the parent Component.
export default function CanvasPanel(props) {
const [sketchCoordinates, setSketchCoordinates] = useState([]);
useEffect(() => {
console.log(sketchCoordinates);
}, [sketchCoordinates]);
const onAddPoint = function (mousePos) {
setSketchCoordinates([
...sketchCoordinates,
{ x: mousePos.x, y: mousePos.y }
]);
};
const onPrintObject = (toPrint) => {
console.log(toPrint);
};
return (
<div className="canvas-panel">
Points:
<hr />
{sketchCoordinates.join(", ")}
<Canvas
sketchCurveCoordinates={sketchCoordinates}
addPoint={onAddPoint}
printObject={onPrintObject}
/>
<Sidepanel createPoint={onAddPoint} />
</div>
);
}
This is the child Component
export default function Canvas(props) {
const getMousePos = function (event) {
const offset = canvasRef.current.getBoundingClientRect();
return {
x: event.clientX - offset.left,
y: event.clientY - offset.top
};
};
const handleOnMouseUp = (event) => {
const mousePos = getMousePos(event);
props.printObject(mousePos);
props.addPoint(mousePos);
};
return (
<div className="canvas-container">
<PureCanvas
handleOnMouseUp={handleOnMouseUp}
/>
</div>
);
};
This is the PureCanvas component
export default class PureCanvas extends Component {
shouldComponentUpdate() {
return false;
}
render() {
return (
<canvas
className="canvas"
width="750"
height="500"
onMouseUp={this.props.handleOnMouseUp}
></canvas>
);
}
}
The code is not complete as I removed all canvas related stuff because it doesn't really matter, but it has enough to explain and reproduce the behaviour. The addPoint function should append an object with x and y properties to the sketchCoordinates array through spread operator and the update function from useState. This works when calling addPoint({x: (any int), y: ({any int}) at the CanvasPanel component, but when calling at the Canvas component through props it works only once. The array appends the first element but then it just replaces that same element with a new one, when it should append that element. So, I know I could do this with classes but I can't understand why it won't work with hooks. The following image shows the console after some updates to de sketchCoordinates array:
Console
EDIT
Please consider this code sandbox for further enlightenment.
This doesn't appear to be an issue with anything specific to the hook: if we replace the <canvas> with a button that just calls handleOnMouseUp on click, it works as expected.
I suspect it's not anything to do with the hook, and perhaps it's some detail that's been simplified out of your example:
Here's a code sandbox with this simplified version, that replaces the canvas with just a button.

React TypeScript - passing a callback function

The following code would have worked happily in JavaScript. I am using React+TypeScript, hence this is a JSX file:
Calling component:
<FiltersPanel onFiltersChanged={() => { console.log('I was called') }} />
Inner Component:
const FiltersPanel = (onFiltersChanged) => {
const handleFiltersChange = () => {
onFiltersChanged(); /* I am getting `TypeError: onFiltersChanged is not a function` */
};
return (<div onClick={handleFiltersChange}/> );
};
export default FiltersPanel;
Why is TypeScript complaining that it cannot find the function, while the function is certainly passed as a prop.
You passed the function as a props so you should receive it to your component as a props like this
const FiltersPanel = (props) => { }
then use it like this
props.onFiltersChanged()
OR you can use destructuring like this
const FiltersPanel = ({onFiltersChanged}) => { }
so you can use it without the props object like this
onFiltersChanged()

How to target a specific item to toggleClick on using React Hooks?

I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.

Resources