I have a variable named movies that can take the form of two shapes defined as the following interfaces:
interface BaseMovie {
id: number;
title: string;
}
interface MultiSearchResult extends BaseMovie {
media_type: "movie" | "tv" | "person";
}
I know that when another variable, triggeredSearch is true, the shape will be MultiSearchResult. The shape will be BaseMovie when triggeredSearch is false.
While using this to render each item
<ul>
{movies.map((movie, i) => (
<li key={i}>{!triggeredSearch ? movie.title : movie.media_type}</li>
))}
</ul>
I receive the following warning:
Property 'media_type' does not exist on type 'BaseMovie | MultiSearchResult'.
Property 'media_type' does not exist on type 'BaseMovie'.ts(2339)
Is there a way, outside of using the as keyword (i.e. (movie as MultiSearchResult).media_type), to declare that a movie is a MultiSearchResult when triggeredSearch is true?
A full example can be found here.
TypeScript can't narrow the type of a variable based on the value of another variable, but it can narrow the type of a union based on one of the union's members. So if you can put triggeredSearch and movies in the same data structure, you can use a discriminated union to distinguish things:
type Example =
{ triggeredSearch: true, movies: null | MultiSearchResult[] }
|
{ triggeredSearch: false, movies: null | BaseMovie[] };
declare const data: Example;
// Side note: Using thea rray index as the key an anti-pattern if the
// arrays contents ever change, more here:
// https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318
const items = data.triggeredSearch
? data.movies?.map((movie, i) => <li key={i}>{movie.media_type}</li>)
: data.movies?.map((movie, i) => <li key={i}>{movie.title}</li>);
const result = <ul>{items}</ul>;
Playground link
If you can't, you can use a type assertion function rather than as, which has the advantage of giving you a runtime error if the assertion is false:
function assertMultiSearchResult(movie: BaseMovie): asserts movie is MultiSearchResult {
if (!("media_type" in movie)) {
throw new Error(`Given 'movie' is not a 'MultiSearchResult');
}
}
Then:
<ul>
{movies?.map((movie, i) => {
let content: string;
if (triggeredSerach) {
assertMultiSearchResult(movie);
content = movie.media_type;
} else {
content = movie.title;
}
return <li key={i}>{content}</li>;
})}
</ul>
More long-winded, but also more typesafe.
Typescript doesn't understand your business context.
Why not just set the media_type property to optional, and type your movies state to MultiSearchResult[] only?
import { useEffect, useState } from "react";
import "./styles.css";
interface BaseMovie {
id: number;
title: string;
}
interface MultiSearchResult extends BaseMovie {
media_type?: "movie" | "tv" | "person";
}
export default function App() {
const [triggeredSearch, setTriggeredSearch] = useState(false);
const [movies, setMovies] = useState<MultiSearchResult[]>([
{ id: 1, title: "Avengers" },
{ id: 2, title: "Eternals" }
]);
useEffect(() => {
setMovies([
{ id: 1, title: "Avengers", media_type: "movie" },
{ id: 2, title: "Eternals", media_type: "movie" }
]);
}, [triggeredSearch]);
return (
<div>
<button onClick={() => setTriggeredSearch(true)}>Trigger Search</button>
<ul>
{movies.map((movie, i) => (
<li key={i}>{!triggeredSearch ? movie.title : movie.media_type}</li>
))}
</ul>
</div>
);
}
Related
I am trying to render React Components based on a JSON response from a CMS system, but I can't seem to render the child item components. Can anyone tell me what I am missing/not understanding here?
Here is my code...
// ComponentRenderer.tsx
export const ComponentsHandler: { [key: string]: any } = {
test: Test,
};
export default function ComponentRenderer(config: IBlock): JSX.Element {
if (typeof ComponentsHandler[config.component] !== 'undefined') {
return React.createElement(
ComponentsHandler[config.component],
{
key: config.id,
...config,
},
config.content &&
(typeof config.content === 'string'
? config.content
: config.content.map((c: IBlock) => ComponentRenderer(c)))
);
}
return React.createElement(() => (
<div>
Error: Unable to load <strong>{config.component}</strong> component.
</div>
));
}
Here is the JSON object I am trying to render:
blocks: [
{
id: '1',
component: BlockType.Test,
content: [],
},
{
id: '2',
component: BlockType.Test,
content: [
{
id: '3',
component: BlockType.Test,
content: [],
},
{
id: '4',
component: BlockType.Test,
content: 'this is a string',
},
],
}
]
export interface IBlock {
id: string;
component: BlockType;
content: IBlock[] | string;
}
export enum BlockType {
Test = 'test',
Hero = 'hero',
Video = 'video',
Menu = 'menu',
Navigation = 'navigation',
Testimonial = 'testimonial',
Card = 'card',
CarTile = 'carTile',
HorizontalScrollSection = 'horizontalScrollSection',
Div = 'div',
Section = 'section',
}
Here is the "Test" component I am looking to render in the JSON response
// TestComponent.tsx
export default function Test(props: IBlock) {
return props.content && <p>{props.id}</p>;
}
// UI HomePage
export default function HomePage() {
return (
mockData &&
mockData.blocks.map((block: IBlock) => (
<ComponentRenderer {...block} key={block.id}></ComponentRenderer>
))
);
}
I would expect the browser to render
1
2
3
4
But instead, I only get the top-level JSON items.
1
2
You created a really cool react recursive structure. A few days ago I tried to do something similar but gave up... I will use yours as an inspiration.
Check if this changes can help you:
IBlock:
export interface IBlock {
id: string;
component: BlockType;
content: IBlock[] | string;
children?: React.ReactNode;
}
TestComponent.tsx
export default function Test(props: IBlock) {
return (
props.content && (
<p>
{props.id} {props.children}
</p>
)
);
}
API Structure
{
"default": [
{
"id": 1,
"name": "A"
},
{
"id": 2,
"name": "B"
},
{
"id": 3,
"name": "C"
},
]
}
i want to handling many props in child component,,, :(
many props example is useState([]), useState(boolean)
export interface iItemListProps {
id: number;
name: string;
}
export interface iDepth {
depth: boolean;
}
function App() {
const [itemList, setItemList] = useState([]);
const [depth, setDepth] = useState(false);
useEffect(() => {
fetch("API URL")
.then((res) => res.json())
.then((res) => {
setItemList(res);
});
}, []);
return (
<div className="wrap">
<ul className="tabList">
<li className="active">1</li>
<li>2</li>
<li>3</li>
</ul>
<div className="listContainer">
<Product itemList={itemList} {...depth} />
</div>
</div>
);
}
component itemList error is
Type 'never[]' is not assignable to type '[{ id: number; name: string; }]'.
Target requires 1 element(s) but source may have fewer.
App.tsx
type iItemListProps = {
itemList: [
{
id: number;
name: string;
}
];
depth: boolean;
};
const Product: React.FunctionComponent<iItemListProps> = ({
depth,
itemList,
}) => {
console.log(depth);
return (
<div className="weekly">
<p className="title">이번주 채소</p>
{itemList.map((item) => (
<span key={item.id} className="item">
{item.name}
</span>
))}
<Allergy />
</div>
);
};
Please tell me if there is a good way.....
The problem lies here: const [itemList, setItemList] = useState([]);
here you are initialising your state with an empty array and not with your itemList type which is an array of object..
Do this:
const [itemList, setItemList] = useState([{id: 0; name: '';}]);
OR
const [itemList, setItemList] = useState([{}]);
Also, with no value in your state and using it with map may cause problem like this.
In your App.tsx use && condition so that it will execute only when you have data in your itemList.
{itemList && itemList.map((item) => (
<span key={item.id} className="item">
{item.name}
</span>
))}
You need to specify what type useState handles, for example:
useState<IItemListProps>([]);
/**
IItemListProps = {
id: number;
name: string;
}[]
*/
You can do it because of useState signature
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
Error happens because [] type by default inferred as never[]
You have to mention the type in useState. For example:
const [itemList, setItemList] = useState<iItemListProps[]>([]);
Or have to give initial state in the useState. For example:
const [itemList, setItemList] = useState([{}]);
Or
const [itemList, setItemList] = useState([{id: 0, name: 'name'}]);
Hope problem is solved :)
I have created a page which has two columns:
In one column the idea is to display a list of items
On the other column, I should show some info related to the selected item
The code I have so far is:
import { INavLink, INavLinkGroup, INavStyles, Nav } from "#fluentui/react";
import React, { createContext, useContext, useState } from "react";
interface HistoryTtem {
id: string;
}
interface AppState {
selectedItem: string | undefined;
updateSelectedItem: (value: string | undefined) => void;
items: Array<HistoryTtem>;
}
const AppContext = createContext<AppState>({
selectedItem: undefined,
updateSelectedItem: (value: string | undefined) => {},
items: []
});
const App = () => {
const Column1 = () => {
const rootState: AppState = useContext(AppContext);
const getNavLinks: Array<INavLink> = rootState.items.map((item) => ({
name: item.id,
key: item.id,
url: ""
}));
const groups: Array<INavLinkGroup> = [
{
links: getNavLinks
}
];
const navStyles: Partial<INavStyles> = {
root: {
boxSizing: "border-box",
border: `1px solid #eee`,
overflowY: "auto"
}
};
const onItemClick = (
e?: React.MouseEvent<HTMLElement>,
item?: INavLink
) => {
if (item && item.key) {
rootState.updateSelectedItem(item.key);
}
};
return (
<Nav
onLinkClick={onItemClick}
selectedKey={rootState.selectedItem}
ariaLabel="List of previously searched transactions"
styles={navStyles}
groups={groups}
/>
);
};
const Column2 = () => {
return <div>aaa</div>;
};
const [historyItems, setHistoryItems] = useState<Array<HistoryTtem>>([
{
id: "349458457"
},
{
id: "438487484"
},
{
id: "348348845"
},
{
id: "093834845"
}
]);
const [selectedItem, setSelectedItem] = useState<string>();
const updateSelectedItem = (value: string | undefined) => {
setSelectedItem(value);
};
const state: AppState = {
selectedItem: selectedItem,
updateSelectedItem: updateSelectedItem,
items: historyItems
};
return (
<AppContext.Provider value={state}>
<div>
<Column1 />
<Column2 />
</div>
</AppContext.Provider>
);
};
export default App;
As you can see, I have a root state which will serve to drive the update of the second column triggered from inside the first one. But it is not working. When I click on an item, the whole component in the first column is re-rendering, while it should only change the selected item.
Please find here the CodeSandbox.
You shouldn't nest component functions.
The identity of Column1 changes for every render of App since it's an inner function, and that makes React think it needs to reconcile everything.
Move Column1 and Column2 up to the module level.
What makes react rerender is two things:
Change in State
Change in Props
You have an App Component which is the root of your components and it has a selectedItem state which is changing when an item is clicked so you have a new state and the new state will cause rerender
I'm following a React course and I'm trying to do some experiments around the code to have a better understanding of concepts.
I have some dummy data:
export const data = [
{ id: 1, name: 'john' },
{ id: 2, name: 'peter' },
{ id: 3, name: 'susan' },
{ id: 4, name: 'anna' },
];
and this is my component:
import React from "react";
import { data } from "../../../data";
const UseStateArray = () => {
const [people, setPeople] = React.useState(data);
return (
<>
{people.map((person) => {
const { id, name } = person;
return (
<div key={id} className="item">
<h4>{name}</h4>
</div>
);
})}
<button
type="button"
className="btn"
onClick={() => setPeople([])}
>
Clear Items
</button>
</>
);
};
export default UseStateArray;
The button has an event handler on click which calls setPeople with an empty array (so to remove all of the elements).
I was trying to change the funcionality of such button, trying to change the name of the first element of my array of objects (data) in the following way:
onClick={() => setPeople(people[0].name = 'Frank')}
Doing this, get an error, namely: "TypeError: people.map is not a function".
I think the reason is because I'm not returning an array anymore and therefore map fails to run.
How can I simply change the name (or any value) of an object present in an array?
You are mutating the object
clickHandler = () => {
const newPeople = people.map((person, index) => {
if (index === 0) {
return {
...person,
name: 'Frank'
}
}
return person;
});
setPeople(newPeople);
}
....
....
onClick={clickHandler}
You need to copy the array into a newer version.
Extract the object out of the array using the index property.
Update the field.
function App() {
const [data, setData] = React.useState([
{ name: "Hello", id: 1 },
{ name: "World", id: 2 }
]);
function changeName(idx) {
const newData = [...data];
newData[idx].name = "StackOverFlow";
setData(newData);
}
return (
<div>
{data.map((d, idx) => {
return (
<div>
<p>{d.name}</p>
<button
onClick={() => {
changeName(idx);
}}
/>
</div>
);
})}
</div>
);
}
NOTE :-
Mutation is not allowed on the state, which basically means you cannot change the state directly. Copy the object or an array and do the updates.
I have that part of code
const links = [
{
name: 'How it works',
ref: '/'
},
{
name: 'Calendar',
ref: 'calendar'
},
{
name: 'Contact me',
ref: 'contact'
}
];
const renderLinks = links.map((link, index) =>
<li className="nav-item active" key={index}>
<a className="nav-link" href={link.ref || "#"}>
{link.name}
</a>
</li>
);
However when I try to render it an error is thrown.
render() {
return (
{renderLinks}
);
}
Objects are not valid as a React child (found: object with keys
{renderLinks}). If you meant to render a collection of children, use
an array instead.
As I think, I have to got an array but React thinks there is an object.
React thinks this is an object because you indeed provided an object. This is what it is if you write it without shortcut property notation:
render() {
return {
renderLinks: renderLinks
);
}
Just return renderLinks directly without { }:
render() {
return renderLinks;
}