React typescript recognizes array as object - reactjs

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>;
}

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.

Dynamic atom keys in Recoil

I'm trying to make a dynamic form where the form input fields is rendered from data returned by an API.
Since atom needs to have a unique key, I tried wrapping it inside a function, but every time I update the field value or the component re-mounts (try changing tabs), I get a warning saying:
I made a small running example here https://codesandbox.io/s/zealous-night-e0h4jt?file=/src/App.tsx (same code as below):
import React, { useEffect, useState } from "react";
import { atom, RecoilRoot, useRecoilState } from "recoil";
import "./styles.css";
const textState = (key: string, defaultValue: string = "") =>
atom({
key,
default: defaultValue
});
const TextInput = ({ id, defaultValue }: any) => {
const [text, setText] = useRecoilState(textState(id, defaultValue));
const onChange = (event: any) => {
setText(event.target.value);
};
useEffect(() => {
return () => console.log("TextInput unmount");
}, []);
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
};
export default function App() {
const [tabIndex, setTabIndex] = useState(0);
// This would normally be a fetch request made by graphql or inside useEffect
const fields = [
{ id: "foo", type: "text", value: "bar" },
{ id: "hello", type: "text", value: "world" }
];
return (
<div className="App">
<RecoilRoot>
<form>
<button type="button" onClick={() => setTabIndex(0)}>
Tab 1
</button>
<button type="button" onClick={() => setTabIndex(1)}>
Tab 2
</button>
{tabIndex === 0 ? (
<div>
<h1>Fields</h1>
{fields.map((field) => {
if (field.type === "text") {
return (
<TextInput
key={field.id}
id={field.id}
defaultValue={field.value}
/>
);
}
})}
</div>
) : (
<div>
<h1>Tab 2</h1>Just checking if state is persisted when TextInput
is unmounted
</div>
)}
</form>
</RecoilRoot>
</div>
);
}
Is this even possible with recoil. I mean it seems to work but I can't ignore the warnings.
This answer shows how you can manually manage multiple instances of atoms using memoization.
However, if your defaultValue for each usage instance won't change, then Recoil already provides a utility which can take care of this creation and memoization for you: atomFamily. I'll quote some relevant info from the previous link (but read it all to understand fully):
... You could implement this yourself via a memoization pattern. But, Recoil provides this pattern for you with the atomFamily utility. An Atom Family represents a collection of atoms. When you call atomFamily it will return a function which provides the RecoilState atom based on the parameters you pass in.
The atomFamily essentially provides a map from the parameter to an atom. You only need to provide a single key for the atomFamily and it will generate a unique key for each underlying atom. These atom keys can be used for persistence, and so must be stable across application executions. The parameters may also be generated at different callsites and we want equivalent parameters to use the same underlying atom. Therefore, value-equality is used instead of reference-equality for atomFamily parameters. This imposes restrictions on the types which can be used for the parameter. atomFamily accepts primitive types, or arrays or objects which can contain arrays, objects, or primitive types.
Here's a working example showing how you can use your id and defaultValue (a unique combination of values as a tuple) as a parameter when using an instance of atomFamily state for each input:
TS Playground
body { font-family: sans-serif; }
input[type="text"] { font-size: 1rem; padding: 0.5rem; }
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/recoil#0.6.1/umd/recoil.min.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.7/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom';
// import type {ReactElement} from 'react';
// import {atomFamily, RecoilRoot, useRecoilState} from 'recoil';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {atomFamily, RecoilRoot, useRecoilState} = Recoil;
const textInputState = atomFamily<string, [id: string, defaultValue?: string]>({
key: 'textInput',
default: ([, defaultValue]) => defaultValue ?? '',
});
type TextInputProps = {
id: string;
defaultValue?: string;
};
function TextInput ({defaultValue = '', id}: TextInputProps): ReactElement {
const [value, setValue] = useRecoilState(textInputState([id, defaultValue]));
return (
<div>
<input
type="text"
onChange={ev => setValue(ev.target.value)}
placeholder={defaultValue}
{...{value}}
/>
</div>
);
}
function App (): ReactElement {
const fields = [
{ id: 'foo', type: 'text', value: 'bar' },
{ id: 'hello', type: 'text', value: 'world' },
];
return (
<RecoilRoot>
<h1>Custom defaults using atomFamily</h1>
{fields.map(({id, value: defaultValue}) => (
<TextInput key={id} {...{defaultValue, id}} />
))}
</RecoilRoot>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
I think the problem is from textState(id, defaultValue). Every time you trigger re-rendering for TextInput, that function will be called again to create a new atom with the same key.
To avoid that situation, you can create a global variable to track which atom added. For example
let atoms = {}
const textState = (key: string, defaultValue: string = "") => {
//if the current key is not added, should add a new atom to `atoms`
if(!atoms[key]) {
atoms[key] = atom({
key,
default: defaultValue
})
}
//reuse the existing atom which is added before with the same key
return atoms[key];
}

react and redux props differences

i have app.js and component.js
component.js is in app.js
in my redux,
there is number and
"number+1" function and
"number-1 "function.
when i use redux props,
export default function Sample2({ props }) {
return (
<div>
<h2>{props.num}</h2>
<button onClick={props.onClickIncrease}>+</button>
<button onClick={props.onClickDecrease}>-</button>
</div>
);
}
this works
and
export default function Sample2( props ) {
return (
<div>
<h2>{props.num}</h2>
<button onClick={props.onClickIncrease}>+</button>
<button onClick={props.onClickDecrease}>-</button>
</div>
);
}
doesn't work
my app.js is
<Sample
num={props.num}
onClickIncrease={props.onClickIncrease}
onClickDecrease={props.onClickDecrease}
/>
i made component like this.
is there difference using {} on props?
Yes of course there is a difference here is a small example perhaps it will explain the situation
const sameFunction1 = ({props}) => {
console.log(props.num)
}
const sameFunction2 = (props) => {
console.log(props.num)
}
sameFunction1({
props: {
num: 1 // <-------
},
num:2
})
sameFunction2({
props: {
num: 1
},
num:2 // <-------
})
To tell the truth, the question does not concern react and redux, the question concerns destructurization.
In the first case we do it, in the second we don't, in the first case we extract props from the object in the second we work with the whole object
In other words:
const sameFunction1 = ({props}) => {
console.log(props.num)
}
it equal below code:
const sameFunction1 = (data) => {
const props = data.props // const {props} = data.props
console.log(props.num)
}

Passing array as props changes type

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}/>
)
}

Array passed as prop gets accepted as a string

Edit: Very stupid overlook on my time. Next time I'll read the documentation more slowly.
Passing an array as a prop, it gets accepted as a string with the same name.
For example if i'm passing usersList = ['Name1', 'Name2', 'Name3'] to a child, the child only has access to usersList as a string, not its content.
Tried doing const Table = ({usersList}) => {...}, const Table = (usersList)=>{...}
The parent that inits the array:
import React from "react";
import { connect } from "react-redux";
import Table from "../presentational/Table.jsx";
const mapStateToProps = state => {
return { users: state.users };
};
const test = ["1", "4", "5"];
const ConnectedPeople = ({ users }) => {
return (
<ul className="list-group list-group-flush">
{console.log(test)}
<Table usersList="{test}" />
</ul>
);
};
const People = connect(mapStateToProps)(ConnectedPeople);
export default People;
The child accepting it
import React from "react";
const Table = usersList => {
return (
<div>
<pre>{Object.keys(usersList)}</pre>
</div>
);
};
export default Table;
I would assume I can just do usersList.map(...), but it comes as an object with every character of the string usersList. so the Object.keys(usersList) renders usersList
The problem is in the way you are passing the prop, which is in the form of a string. Instead you need to write
<Table usersList={test} />
Once you make the above change, you can simply access the array elements by mapping over the props like
this.props.usersList.map(...)
problem:
<Table usersList="{test}" />
fix:
<Table usersList={test} />

Resources