React - how to determine if a specific child component exists? - reactjs

If I have this structure:
const MyComponent = (props) => {
return (
<Wrapper />{props.children}</Wrapper>
);
}
and I use it like this:
<MyComponent>
<SomeInnerComponent />
</MyComponent>
How can I check to see if <SomeInnerComponent /> has specifically been included between <MyComponent></MyComponent>, from within the MyComponent function?

Given that you want to check that SomeInnerComponent is present as a child or not, you could do the following
const MyComponent = (props) => {
for (let child in props.children){
if (props.children[child].type.displayName === 'SomeInnerComponent'){
console.log("SomeInnerComponent is present as a child");
}
}
return (
<Wrapper />{props.children}</Wrapper>
);
}
Or you could have a propTypes validation on your component
MyComponent.propTypes: {
children: function (props, propName, componentName) {
var error;
var childProp = props[propName];
var flag = false;
React.Children.forEach(childProp, function (child) {
if (child.type.displayName === 'SomeInnerComponent') {
flag = true
}
});
if(flag === false) {
error = new Error(componentName + ' does not exist!'
);
}
return error;
}
},

Just want to provide an answer for a similar but different need, where you might have an HOC wrapping several layers of components, and you'd like to see if the HOC has already wrapped a component. The method I came up with was to have the HOC add a data-attribute onto the component to serve as a flag, which the HOC could then check on subsequent runs.
const WithFoo = Component = props => {
return props["data-with-foo"]
? <Component {...props} />
: (
<FooWrapper>
<Component {...props} data-with-foo />
</FooWrapper>
);
};

React nodes have a type property that's set to the constructor, or the function that created the node. So I wrote the following function for checking if a React node was created by a certain component.
const reactNodeIsOfType = (node, type) =>
typeof node === "object" && node !== null && node.type === type;
Usage:
const MyComponent = (props) => {
let hasInnerComponent = false;
React.Children.forEach(props.children, (child) => {
if (reactNodeIsOfType(child, SomeInnerComponent)) {
hasInnerComponent = true;
}
});
return (
<>
<div>hasInnerComponent: {hasInnerComponent ? "yes" : "no"}.</div>
<Wrapper>{props.children}</Wrapper>
</>
);
}
<MyComponent><div /></MyComponent>
// hasInnerComponent: no
<MyComponent><SomeInnerComponent /></MyComponent>
// hasInnerComponent: yes
const c = <SomeInnerComponent />;
console.log(reactNodeIsOfType(c, SomeInnerComponent));
// true
This is the TypeScript version. Calling it will also assign the correct type to the node's props.
const isObject = <T extends object>(value: unknown): value is T =>
typeof value === "object" && value !== null;
const reactNodeIsOfType = <
P extends object,
T extends { (props: P): React.ReactElement | null | undefined }
>(
node: React.ReactNode,
type: T
): node is { key: React.Key | null; type: T; props: Parameters<T>[0] } =>
isObject<React.ReactElement>(node) && node.type === type;

Related

Next/React - How to correctly build up state from multiple child components

I think this is easier to explain using a codesandbox link. This follows on from a previous question of mine, which may help provide more overall context. Currently, when interacting with the child elements (i.e. inputs), the state updates to {"values":{"0":{"Text1":"test"},"1":{"bool":true}}}. The issue is that if you interact with the other inputs within a Parent component, e.g. Text2 in the Parent component with id 0, it will overwrite the value already in the state, which makes it look like this {"values":{"0":{"Text2":"test"},"1":{"bool":true}}}. I want it to look like {"values":{"0":{"Text1":"test", "Text2":"test"},"1":{"bool":true}}}.
This is my try with your problem. I would like to have childIndex instead of number like you. It would be easier to work with other components later.
Here is my codesandbox
import { useEffect, useState } from "react"
import Parent from "./Parent"
const id1 = 0
const id2 = 1
interface Boo {
childIndex: number
value: {
[name: string]: string | boolean
}
}
const GrandParent: React.FC = () => {
const [values, setValues] = useState<Boo[]>([])
const valuesChange = (e: React.ChangeEvent<HTMLInputElement>, id: number) => {
console.log("change event")
const name = e.target.name
let value: any
if (name === "bool") {
value = e.target.checked
} else {
value = e.target.value
}
setValues((prev) => {
// Update new value to values state if input already there
let updateBoo = prev.find((boo) => boo.childIndex === id)
if (updateBoo) {
// Update Values
const valKeys = Object.keys(updateBoo.value)
const valIndex = valKeys.find((val) => val === name)
if (valIndex) {
updateBoo.value[valIndex] = value
} else {
updateBoo.value = { ...updateBoo.value, [name]: value }
}
} else {
// Create new if not added
updateBoo = {
childIndex: id,
value: { [name]: value }
}
}
return [...prev.filter((boo) => boo.childIndex !== id), updateBoo]
})
}
useEffect(() => {
console.log("Render", { values })
})
return (
<>
<div>{JSON.stringify({ values }, undefined, 4)}</div>
<br />
<br />
<Parent valueChange={(e) => valuesChange(e, id1)} key={id1} />
<Parent valueChange={(e) => valuesChange(e, id2)} key={id2} />
</>
)
}
export default GrandParent
The trick is you should return the previous state of the property too:
setValues((prev) => {
return {
...prev,
[id]: {
...(prev && (prev[id] as {})), // <= return the previous property state
[name]: value
}
}
})
I'm not very good at typescript but I tried my best to solve some types' errors
you can see an example below

Variable number of generic arguments for React component in Typescript

In ReactJS with TypeScript (both latest stable at the time of writing of this post) I can define a component that conditionally renders its children if and only if a given property is truthy. One possible implementation could be:
// I will be using this type helper in subsequent examples
export type Maybe<T> = T | undefined | null
export function Only<T>(props: {
when: Maybe<T>
children: ReactNode
}) {
const {when, children} = props
return <>{when ? children : null}</>
}
It can then be used in the following manner:
const MyComponent = (props: { value: number }) => (
<div>Value: {props.value}</div>
);
export function MyPage(){
const [value, setValue] = useState<number>()
// value: number | undefined
setTimeout(()=>{
// Simulate a request
setValue(42)
}, 1000)
return (
<div>
<Only when={value}>
<MyComponent value={value!}/>
</Only>
</div>
)
}
It behaves as desired, but the issue is that I have to use the non-null assertion operator ! in order to pass the value to MyComponent. Fortunately, I am able to solve it by passing the narrowed type to children:
export function Only<T>(props: {
when: Maybe<T>
children: ReactNode | ((when: T) => ReactNode)
}) {
const {when, children} = props;
return when ?
<>{children instanceof Function ? children(when) : children}</>
: null
}
I would then use it in the following manner:
<Only when={value}>{value => // Overshadowing with narrowed type
<MyComponent value={value}/>
}</Only>
Once again, this works as expected, albeit introducing a little bit of verbosity.
But what if I need to conditionally render a component based on "truthiness" of more than one property and pass them down to children with narrowed types? One obvious solution is to nest the aforementioned Only components:
const MyComponent = (props: { value1: number, value2: string }) => (
<>
<div>Value1: {props.value1}</div>
<div>Value2: {props.value2}</div>
</>
);
export function MyPage() {
const [value1, setValue1] = useState<number>()
const [value2, setValue2] = useState<string>()
setTimeout(() => {
// Simulate a request
setValue1(42)
setValue2('foo')
}, 1000)
return (
<div>
<Only when={value1}>{value1 =>
<Only when={value2}>{value2 =>
<MyComponent value1={value1} value2={value2}/>
}</Only>
}</Only>
</div>
)
}
Even though it will work with any number of properties, it is easy to see how this approach is not scalable given the sheer amount of boilerplate code we would need to write in order to pass several properties this way. Ideally, we would pass those properties in an array, whose members will be narrowed down in a similar manner. For example, a special case for 2 properties would look like this:
export function Only2<T, S>(props: {
when: [Maybe<T>, Maybe<S>]
children: ReactNode | ((when: [T, S]) => ReactNode)
}) {
const {when, children} = props
return when[0] && when[1] ?
<>{children instanceof Function ? children([when[0], when[1]]) : children}</>
: null
}
It would then be used in the following manner:
<Only2 when={[value1, value2]}>{([value1, value2]) =>
<MyComponent value1={value1} value2={value2}/>
}</Only2>
Unfortunately, I am unable to come up with a solution for a general case, where we would be able to pass and narrow down types of any number of values in a convenient manner as shown in the snippet above. Hence my question is twofold: Is it even possible to achieve the result that I am looking for in TypeScript? And if not, are there any alternative strategies for achieving a generic component that could be used in a convenient manner as was shown in the snippets that used Only component?
You can use tuples here.
// Helper type to remove nulls from array
type RemoveNulls<T> = {
[K in keyof T]: NonNullable<T[K]>
}
// simplified example of a function
// its generic parameter is a tuple
function nonNullableArray<T extends readonly any[]>(items: T, fn: (items: RemoveNulls<T>) => void) {
if (items.some(item => item === null)) {
return;
}
// Unfortunately, we have to cast type here.
// I can't think of any other way to narrow type of array by checking it's items
fn(items as RemoveNulls<T>);
}
declare const a: string | null;
declare const b: number | null;
// `as const` is an important part. It helps typescript to narrow down type
// from array to tuple
// (string | number | null)[] vs [string | null, number | null]
const v = nonNullableArray([a, b] as const, ([a, b]) => {
console.log(a + b)
})
Here's an approach that uses object types, and illustrates use with React components. I assumed that you have an object type without null/undefined allowed, and added that as an option.
import React, {ReactNode} from 'react';
type OrNull<T> = {
[P in keyof T]: T[P] | undefined | null;
};
function Only<T>(props: {
when: OrNull<T>,
children: ReactNode | ((when: T) => ReactNode)
}) {
const {when, children} = props;
for (const key in when) {
if (when[key] == null) return null;
}
return <>
{children instanceof Function ? children(when as T) : children}
</>;
}
type FooBar = {
foo: string,
bar: boolean,
}
const child = ({foo, bar}: FooBar) =>
<span>{foo} is {bar}</span>;
<>
<Only<FooBar> when={{foo: 'hi', bar: true}}>
{child}
</Only>
<Only<FooBar> when={{foo: 'hi', bar: undefined}}>
{child}
</Only>
</>
Playground link
Thanks to #edemaine for his brilliant answer. In this answer I merely adapt his solution to the examples I provided in my question, just for the sake of competition, in case someone might find it useful:
import React, {ReactNode, useState} from "react"
type OrNull<T> = {
[P in keyof T]: T[P] | undefined | null
}
export function Only<T>(props: {
when: OrNull<T> | boolean // Allows usage of boolean literals
children: ReactNode | ((when: T) => ReactNode)
}) {
const {when, children} = props
if (typeof when === 'boolean') {
if (!when)
return null
} else {
for (const key in when) {
if (!when[key])
return null
}
}
return <>{children instanceof Function ? children(when as T) : children}</>
}
const MyComponent1 = (props: { value1: number }) => (
<>
<div>Value1: {props.value1}</div>
</>
)
const MyComponent23 = (props: { value2: string, value3: boolean }) => (
<>
<div>Value2: {props.value2}</div>
<div>Value3: {Boolean(props.value3).toString()}</div>
</>
)
export function MyPage() {
const [value1, setValue1] = useState<number>()
const [value2, setValue2] = useState<string>()
const [value3, setValue3] = useState<boolean>()
setTimeout(() => {
// Simulate a request
setValue1(42)
setValue2('foo')
setValue3(true)
}, 1000)
return (
<div>
<Only when={{value1, value2, value3}}>{({value1, value2, value3}) =>
<>
<MyComponent1 value1={value1}/>
<MyComponent23 value2={value2} value3={value3}/>
</>
}</Only>
</div>
)
}

How to pass conditional props to unknown type of react component in typescript?

I have this components:
export function Hover(props: PolygonProps) {
//...
}
export function Route(props: CenterProps) {
//...
}
This enum:
export enum Highlight {
HOVER,
ROUTE,
}
This object:
export const HIGHLIGHTS = {
[Highlight.HOVER]: Hover,
[Highlight.ROUTE]: Route,
};
And this code in render:
let HighlightComponent;
сonst center = props.center;
const points = props.points;
if (highlight !== undefined) {
HighlightComponent = HIGHLIGHTS[highlight] as React.ComponentType<CenterProps | PolygonProps>;
}
const centerProps: CenterProps = { center };
const polygonProps: PolygonProps = { points };
return <div>{HighlightComponent && <HighlightComponent />}</div>
Question: how to pass props of needed type (CenterProps or PolygonProps) if i dont know type?
Is it correct structure for the case or not?
Since you have two seperate props, you must still check to see which one to use. So I would drop the HIGHLIGHTS constant and simply use a switch:
const centerProps: CenterProps = { center: 'someValue' };
const polygonProps: PolygonProps = { points: 'someOtherValue' };
let highlightComponent;
if (highlight !== undefined) {
switch (highlight) {
case Highlight.HOVER:
highlightComponent = <Hover {...polygonProps} />;
break;
case Highlight.ROUTE:
highlightComponent = <Route {...centerProps} />;
break;
}
}
return <div>{highlightComponent}</div>
Another option would be to define the props like this:
const centerProps: CenterProps = { center: 'someValue' };
const polygonProps: PolygonProps = { points: 'someOtherValue' };
export const HIGHLIGHT_PROPS = {
[Highlight.HOVER]: polygonProps,
[Highlight.ROUTE]: centerProps,
};
And then:
let HighlightComponent;
let highlightProps;
if (highlight !== undefined) {
HighlightComponent = HIGHLIGHTS[highlight] as React.ComponentType<CenterProps | PolygonProps>;
highlightProps = HIGHLIGHT_PROPS[highlight] as CenterProps | PolygonProps;
}
return <div>{HighlightComponent && <HighlightComponent {...highlightProps} />}</div>
If I understood correctly you can simply do:
if (highlight !== undefined) {
HighlightComponent = HIGHLIGHTS[highlight] === HIGHLIGHTS.HOVER
? <Hover {...polygonProps}/>
: <Route {...centerProps}/>;
}
return <div>{HighlightComponent && <HighlightComponent />}</div>
However this looks like a code smell so I would suggest that you change your props to be HighlightComponent: JSX.Element and just pass in the relevant component.

Getting the useState from child functional component and appending it the parent useState array of objects

I have a parent component that holds the values of all its child components' information within an array of objects. I'm trying to spawn a new child and keep track of the information within those children in my parent's state as an array of objects. This is where I'm unaware of how to approach this on both fronts; keeping track of the objects within the state and getting the content from the children.
I have a parent component:
const Parent: React.FC = () => {
const [children, setChildren] = React.useState(
[
{
id:1,
name:'',
age:0,
}
]
);
const getChildName = () => {
// not sure how to target the new child
}
// Create a new child
const AddChild = () => {
let tmpid = children[children.length].id + 1; // not sure if this would work
setChildren(
[
...children,
{
id:tmpid,
name:' ',
age:0,
}
]
)
}
return (
<button onClick={this.addChild}>Add component</button>
{
children.map((child) => (
<NewChild key={child.id} getChildName={getChildName} getChildAge={getChildAge}/>
))
}
)
}
and then a simple child component:
interface IChild {
name?: string;
age?: number;
}
type Props = {
getChildName : void,
getChildAge : void,
}
const NewChild: React.FC<Props> = (Props) => {
const [child, setChild] = React.useState<Partial<IChild>>({});
return (
<input value={child?.name??''} onChange={e => setChild({...child , child: e.target.value})}/>
<input value={child?.age??''} onChange={e => setChild({...child , child: parseFloat(e.target.value)})}/>
)
}
Take a look at how I implemented the getChildName below. It is not tested, but I think it should work. Also take a look at the comments in the code. If you need to implement getChildAge, you can do the same as with getChildName, but return newChild.age instead.
const Parent: React.FC = () => {
const [children, setChildren] = React.useState(
[
{
id:1,
name:'',
age:0,
}
]
);
const getChildName = (id: number) => {
let newChild = null;
children.forEach((child) => {
if (child.id == id) {
newChild = child;
}
}
return !!newChild ? newChild.name : `No child was found with id ${id}`
}
// Create a new child
const AddChild = () => {
/* let tmpid = children[children.length].id + 1; -- this will work. */
let tmpid = children.length + 1 // This is cleaner, and as long as your id starts on 1,
// this will be the exact same result.
setChildren(
[
...children,
{
id:tmpid,
name:' ',
age:0,
}
]
)
}
return (
<button onClick={this.addChild}>Add component</button>
{
children.map((child) => (
// You need to add return here.
return <NewChild key={child.id} getChildName={(child.id) => getChildName(child.id)} getChildAge={getChildAge}/>
))
}
)
}
And like johnrsharpe said, don't manage the same state in two places. You need to give NewChild a callback like this:
<NewChild // Inside Render() method of <Parent />
key={child.id}
getChildName={(child.id) => getChildName(child.id)}
getChildAge={getChildAge}
callback={updateChildren}
/>
const updateChildren = (inputChild: IChild) => { // Method of <Parent />
const newChildren = children.map((child) => {
if (child.id = inputChild.id) {
child.name = inputChild.name;
child.age = inputChild.age;
}
return child;
}
setChildren([ ...newChildren ]);
}
And in NewChild component instead of using setChild state, you pass in the object to a function like
const changeChildProperty = (value: any) => {
// update property of child
// Pass in the entire child object, and it will be update in parent state through the callback
props.callback(child);
}

Call a function in a component from antoher file in react

I'm learning reactjs and I'm stuck calling a function in another component.
I did:
import moment from 'moment';
import WeatherLocation from './../components/WeatherLocation'
const transformForecast = datos =>(
datos.list.filter(item => (
moment.unix(item.dt).utc().hour() === 6 ||
moment.unix(item.dt).utc().hour() === 12 ||
moment.unix(item.dt).utc().hour() === 18
)).map(item => (
{
weekDay: moment.unix(item.dt).format('ddd'),
hour: moment.unix(item.dt).hour(),
data: WeatherLocation.getDatos(item)
}
))
);
export default transformForecast;
getDatos is a function in WeatherLocation, I exported WeatherLocation but I don't know what if that calling is correct.
WeatherLocation component:
const api_key = "bb7a92d73a27a97e54ba00fab9d32063";
class WeatherLocation extends Component{
constructor({ city }){
super();
this.state = {
city,
primero: null
}
}
getWeatherState = weather => {
const { id } = weather[0];
if (id < 300){
return THUNDER;
}else if (id < 400){
return DRIZZLE;
}else if (id < 600){
return RAIN;
}else if (id < 700){
return SNOW;
}else if (id >= 800){
return SUN;
}else{
return CLOUDY;
}
};
getTemp = kelvin =>{
return convert(kelvin).from('K').to('C').toFixed(2);
}
getDatos = (weather_data) =>{
const {weather} = weather_data;
const {humidity, temp} = weather_data.main;
const {speed} = weather_data.wind;
const weatherState = this.getWeatherState(weather);
const temperature = this.getTemp(temp);
const primero = {
humidity,
temperature,
weatherState,
wind: `${speed}`,
}
return primero;
};
componentWillMount() {
this.handleUpdateClick();
}
handleUpdateClick = () => {
const {city} = this.state;
const urlTiempo = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${api_key}`;
fetch(urlTiempo).then(primero => {
return primero.json();
}).then(weather_data => {
const primero = this.getDatos(weather_data);
this.setState({primero});
});
};
render = () => {
const {onWeatherLocationClick} = this.props;
const {city, primero} = this.state;
return (
<div className='weatherLocationCont' onClick = {onWeatherLocationClick}>
<Location city={city}/>
{primero ? <WeatherData datos = {primero}/> : <CircularProgress size={60} thickness={7} />}
</div>);
};
}
WeatherLocation.propTypes = {
city: PropTypes.string.isRequired,
onWeatherLocationClick: PropTypes.func
}
export default WeatherLocation;
As you can see I want to reuse getDatos because I'm going to need those variable in transformForecast.
I will appreciate your help, thanks.
WeatherLocation is a React component, not a plain JS object, so you can't just call its internal functions as you please: as just a class definition there is nothing to call yet, you need an instance.
So, you'll need to create an actual <WeatherLocation.../> component on your page/in your UI, and then use the WeatherLocation's documented API for getting its data based on changes in the component, passing it on to whatever is calling the transformForecast function.
Object.Method() call is not allowed here. You need to create a React Stateful or Stateless component and pass props to it from the parent component. Let us say, WeatherLocation is your parent component and transformForecast is the child component. You can do something like this to call your method in WeatherLocation component.
Parent Component:
class WeatherLocation extends React.Component {
constructor(props) {
super(props);
this.state = {
datos: []
};
this.getDatos = this.getDatos.bind(this);
};
getDatos = (item) => {
console.log(item);
};
render() {
return (
<div>
<TransformForecast
getDatos={this.getDatos}
datos={this.state.datos}
/>
</div>
)
}
}
export default WeatherLocation;
Child Component:
const TransformForecast = (props) => {
return (
props.datos.list.filter(item => (
moment.unix(item.dt).utc().hour() === 6 ||
moment.unix(item.dt).utc().hour() === 12 ||
moment.unix(item.dt).utc().hour() === 18
)).map(item => (
{
weekDay: moment.unix(item.dt).format('ddd'),
hour: moment.unix(item.dt).hour(),
data: props.getDatos(item)
}
))
);
};
export default TransformForecast;
Note: This code might not be the right away working code as I'm not sure of the data and API's called. This is just to illustrate to solve your problem.
Hope this helps.

Resources