Let's say I'd like to render a different child component depending on which button has been clicked:
import { useState } from 'react';
const Bold = ({ text }) => (
<b>{text}</b>
);
const Italic = ({ text }) => (
<i>{text}</i>
);
export default function App() {
const [Component, setComponent] = useState();
return (
<>
<button onClick={() => setComponent(Bold)}>
Bold
</button>
<button
onClick={() => setComponent(Italic)}
>
Italic
</button>
{Component && (
<Component text="I love 🧀 more than life!" />
)}
</>
);
}
I was pretty sure this would work - I set my state item to component's function and render it. But it throws an error:
Uncaught TypeError: Cannot destructure property 'text' of '_ref' as it is undefined.
I checked if it would work without any props in children components, but no - it gives another error when there are no props:
Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: . Did you accidentally export a JSX literal instead of a component?
I don't quite understand why it's happening, but I found a workaround. Instead of setting my state value directly like this:
<button onClick={() => setComponent(Bold)}>
I'm setting it through a setter function:
<button onClick={() => setComponent(() => Bold)}>
It works correctly after that change, but can someone explain why is this happening? Is having a component unpacked somewhere in the template causing problems?
The problem is that setState has two forms.
One where it accepts some object and one that it accepts a function which can work with existing state and returns the new state.
Now, when you pass a component reference, you are actually passing a function to it, and so setState assumes it is the second form, and tries to execute the function in order to get the updated state.
The workaround, as you have found out on your own, is to use the second form, passing a function that when executed will return the component you want.
Related
I want to dynamically render a component based on what the user has clicked. I tried something like this:
function ComponentTest() {
const [component, setComponent] = useState<ReactNode | null>(null);
return <button onClick={() => setComponent(SomeFunctionalComponent)}>Crash</button>
}
In this example I'm obviously not doing anything with the state, but clicking this button results in the application crashing with the following error messages:
Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see https://reactjs.org/link/rules-of-hooks
Warning: React has detected a change in the order of Hooks called by EinstellungenTest. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see https://reactjs.org/link/rules-of-hooks
Uncaught Error: Rendered more hooks than during the previous render.
The above error occurred in the <ComponentTest> component:
Now, I could just write
return <button onClick={() => setComponent(<SomeFunctionalComponent/>)}>Crash</button>
instead, but I think that creates the component too early. I want to create the component during the render, like this:
function ComponentTest() {
const [component, setComponent] = useState<ReactNode | null>(null);
return <component/>
}
I hope someone can help me out with this.
You could hold the value of what component you want to use in the state and then use conditional rendering to render the one you want.
Something like this
function ComponentTest() {
const [componentId, setComponentId] = useState<string | null>(null);
return <button onClick={() => setComponent("myId")}> {componentId === "myId" && < SomeFunctionalComponent /> }</button>
}
I have a Table component with generics and I use a static component inside the Table to compose columns, also with generics.
The problem is that I would like the Column component to inherit the generic type passed to the Table when executing the data function, which gets the result passed to it.
I know typing explicitly will work, for example:
<Table.Column<TableItem> data={(item) => item.name} />
The real question is: Is it possible to perform this type inference from my Table component and pass it to the static Column component? So that I don't have to add the type explicitly to each column...
SANDBOX TO REPRODUCE, ONLY TABLE AND COLUMN COMPONENTS:
https://codesandbox.io/s/generic-table-column-r54lh?file=/src/App.tsx
You can specify the type for item:
<Table.Column<TableItem> data={(item:[type goes here]) => item.name} />
Few examples :
<Table.Column<TableItem> data={(item:any) => item.name} />
// or even
<Table.Column<TableItem> data={(item:{name:string;}) => item.name} />
There is an issue in your typing of children, namely that you are using ReactElement which does not include typing for the actual JSX function, just the returned element. You can see this in your example, by noting that the following covariant and contravariant children both pass, notwithstanding your children typing:
const MyDiv = (props:{myProp:string})=><div>{props.myProp}</div>
const App = () => {
return (
<Table<TableItem> keyExtractor={(item) => item.id} values={items}>
<Table.Column header="Name" data={(item) => item.name} />
<Table.Column header="Surname" data={(item) => item.surname} />
<MyDiv myProp="me" />
<div />
</Table>
);
};
BUT... I don't think that's really your underlying issue. The problem really is that you want Typescript to check that your children components are actually created via specific JSX with specific props, but, in fact, all Typescript will currently do is check that your JSX returns a valid JSX.Element. It won't check that the function that generated that JSX.Element satisfies a particular type or that the return type of that function is a specific kind of element with specific props.
This is a design limitation that is discussed in this open issue.
I think you could work around this by essentially defining your own custom prop that took in the props for Table.Column and then passed them to Table.Column inside your Table component. E.g, something like columnProps: ColumnProps<T> as a prop of Table. I'm not sure that would be any easier than just providing the explicit type to Table.Column, however.
I've got a problem in my project which is pretty large therefore I can't post every part of the code here, but I'll try to summarize the main parts.
I've got a parent component which state is managed through a useReducer and which render returns a mapping of this state.
Each child has to take both the value and the index of the mapping. I'm also using the Context API to pass the dispatcher
on some of the childs.
function ParentComponent(props) {
const [state, dispatch] = useReducer(initialData, reducer);
return (
<div>
<MyContext.Provider value={dispatch}>
{state.myArray.map((value, index) => (
<ChildComponent value={value} index={index} />
))}
</MyContext.Provider>
</div>
);
}
/* Other file */
function ChildComponent({value, index}) {
const dispatch = useContext(MyContext);
return <div>
{/* Uses value and index to display some data */}
</div>
}
export default React.memo(ChildComponent, (prevProps, nextProps) => !_.isEqual(prevProps, nextProps));
Some of the childs in the tree components have to use React.memo to avoid useless re-renders, ChildComponent is one of them.
I'm using the lodash functions _.isEqual to compare the props to know when the component has to re-render.
The problem is the following. One of my components in the component tree adds items to the myArray attribute of the ParentComponent state.
When I add an item, anyway, each ChildComponent rendered from the ParentComponent mapping receives the same index of 0, creating a lot of problems.
The ParentComponent has the right indices (when I log them inside the mapping they are the right ones) but it's like the ChildComponent isn't getting it.
Is there any way to solve this? Maybe it has something to do with React.memo?
React.memo() takes a callback that should determine whether prevProps and nextProps are equal, but you're returning the negation of that.
Also using the index as a key should be considered a last resort, since this will fail to behave correctly when elements in the array are re-ordered relative to each other. You should always source the key from the data whenever possible.
I am having some OOP issues that are probably pretty simple. I have a class that renders some html. However it has an onClick that calls a function that sets a flag inside the class if the image is clicked. Now here is the issue, when I render this class object and click the button from a separate js file, it stays false. I want it to permanently change the flag to true when clicked. here is the class...
class Settings extends React.Component {
handleClick() {
this.flag = true;
console.log(this.flag)
}
render(){
return(
<img src="./img/leaf.png" alt="" onClick={() => this.handleClick()}/>
);
}
}
and here is the code that calls it from a separate file...
const settingsObj = new Settings();
console.log(settingsObj.flag);
I want the flag to be false until the button is clecked and then it permamently changes to true. But it only goes true until my page rerenders as new data comes in and it resets to false. I have tried constructors and a few other techniques with no success.
Normal OOP design principles don't always apply directly to React components. Components don't usually have instance properties, they mostly just have props and state (there are a few exceptions where you do use an instance property, like Animation objects in react-native, but these are rare).
You're kind of mixing the two things in a way that doesn't quite make sense here. Settings is a React component that renders an image, but it's also an object which you instantiate by calling new Settings(). If there are other components which depend on the value of flag, you might want to separate the accessing and storing of the flag from the render component, passing a value and a callback to the renderer.
const Settings = ({setFlag}) => {
return(
<img src="./img/leaf.png" alt="" onClick={() => setFlag(true)}/>
);
}
You've suggested that you like the Context API as a solution for making the flag value globally available. There are a few ways to set this up, but here's one.
Outside of any component, we create a FlagContext object that has two properties: a boolean value flag and callback function setFlag. We need to give it a default fallback value, which is hopefully never used, so our default callback just logs a warning and does nothing.
const FlagContext = createContext<FlagContextState>({
flag: false,
setFlag: () => console.warn("attempted to use FlagContext outside of a valid provider")
});
This FlagContext object gives up Provider and Consumer components, but it's up to us to give a value to the FlagContext.Provider. So we'll create a custom component that handles that part. Our custom FlagProvider uses a local state to create and pass down the value. I've used a function component, but you could use a class component as well.
const FlagProvider = ({children}) => {
const [flag, setFlag] = useState(false);
return (
<FlagContext.Provider value={{
flag,
setFlag
}}>
{children}
</FlagContext.Provider>
)
}
We want to put the entire App inside of the FlagProvider so that the whole app has the potential to access flag and setFlag, and the whole app gets the same flag value.
When you want to use the value from the context in a component, you use either the useContext hook or the Consumer component. Either way, I like to creating an aliased name and export that rather than exporting the FlagContext object directly.
export const FlagConsumer = FlagContext.Consumer;
export const useFlagContext = () => useContext(FlagContext);
With the Consumer, the child of the consumer is a function that takes the value of the context, which in out case is an object with properties flag and setFlag, and returns some JSX.
This is usually a function you define inline:
const SomePage = () => {
return (
<FlagConsumer>
{({flag, setFlag}) => (<div>Flag Value is {flag.toString()}</div>)}
</FlagConsumer>
)
}
But it can also be a function component. Note that when using a function component as the child, you must pass the component itself ({Settings}) rather than an executed version of it (<Settings />).
const Settings = ({ setFlag }) => {
return <img src="./img/leaf.png" alt="" onClick={() => setFlag(true)} />;
};
const SomePage = () => {
return <FlagConsumer>{Settings}</FlagConsumer>;
};
The preferred method nowadays is with hooks. We call useFlagContext() inside the body of the function component and it returns our context object.
const SomePage = () => {
const {flag, setFlag} = useFlagContext();
return <Settings setFlag={setFlag}/>
};
Both the consumer and the hook only work if they are inside of a flag context provider, so that's why we put it around the whole app!
const App = () => {
return (
<FlagProvider>
<SomePage />
</FlagProvider>
);
};
Complete example on CodeSandbox
For this kind of interactions, I highly recommend you to use Redux
Another think I'm sure you will benefit from, is switching to hooks and function components: less boilerplate and much flexible code.
Back to the goal, using Redux your code would look similar to this:
const Settings = (props) => {
const dispatch = useDispatch();
const flag = useSelector(state => state.yourStoreObj.flag);
handleClick() {
dispatch(yourCustomAction("UPDATE_FLAG", true));
}
return(
<img src="./img/leaf.png" alt="" onClick={() => handleClick()}/>
);
}
Explanation:
First of all, spend 15 mins and get used to React Redux. Here's a good practical article to start with. If you're not familiar with hooks, start learning them as that will change a lot, while you don't need to change a single line of what you've done so far.
We suppose there's a property in the store that is the "flag" property of that specific element. In this way, the property can be read by the component itself with the useSelector() operator, or can be read anywhere in your application with the same methodology from any other component.
In the same way, you can change the value by dispatching a change (see dispatch() function) and in the same way, you can do that from any other components.
So, let's say you want to change that property when a click occurs on a completely different component, this is how the other component may looks like
const OtherCoolComp = (props) => {
const dispatch = useDispatch();
handleClick() {
dispatch(yourCustomAction("UPDATE_FLAG", true));
}
return(
<button onClick={() => handleClick()}>
Click me!
</button>
);
}
So you're dispatching the same action, setting it to the value you prefer, from a component that doesn't know who is displaying that value.
I'm working on the freeCodeCamp drum machine app. In my app with function arrow components, I set state of display with the useState hook in the parent component and pass it as a prop to the child component. In the parent component, I try to render the display state in a div. However, when the method is triggered (on click of the "drum pad" div), the app crashes. In the console I get an error that says "Uncaught Invariant Violation: Objects are not valid as a React child (found: object with keys {display}). If you meant to render a collection of children, use an array instead."
I've been following along a YouTube tutorial for this project but using arrow function components and Hooks instead of regular classes as used in the tutorial--in the tutorial (around 1:55 of this video) the person successfully does what I'm trying to do, so I think the issue is something to do with using Hooks or arrow function components.
// APP COMPONENT (PARENT)
const sounds = [
{ id: 'snare', letter: 'Q', src: 'https://www.myinstants.com/media/sounds/snare.mp3' },
// etc.
];
const App = () => {
const [display, setDisplay] = useState(''); // <----
const handleDisplay = display => { // <----
setDisplay({ display });
}
return (
<div className="App">
<div className="drum-machine">
<div className="display">
<p>{display}</p> // <---- Related to error in console
</div>
<div className="drum-pads">
{sounds.map(sound => (
<DrumPad
id={sound.id}
letter={sound.letter}
src={sound.src}
handleDisplay={handleDisplay} // <----
/>
))}
</div>
</div>
</div>
);
}
// DRUMPAD COMPONENT (CHILD)
const DrumPad = ({ id, letter, src, handleDisplay }) => {
let audio = React.createRef();
const handleClick = () => {
audio.current.play();
audio.current.currentTime = 0;
handleDisplay(id); // <----
}
return (
<div
className="drum-pad"
id={id}
onClick={handleClick}
>
<p className="letter">{letter}</p>
<audio
ref={audio}
id={letter}
src={src}
>
</audio>
</div>
);
}
You're setting the state as an object instead of a string. Remove the curly brackets around it.
const handleDisplay = display => {
setDisplay(display);
}
This was already answered, but since you are following a tutorial, I am assuming you are learning React and wanted to point a couple of things to help you :)
The incorrect use of state was pointed out, but just for clarification (and the reason I think you were using an object): in the "old" way, with Class components, the state used to be an object, and you needed to update it like an object. This example here shows that. With Hooks, you don't need to set the whole State object, only that specific state property. More info here.
Another point is, in your CodePen example at least, you were missing the import for useState. You either need to import it like this import { useState } from React or use it like this React.useState, since this is a separate module, not imported by default when you import React.
The last point is, when creating components using a loop (like your <DrumPad> with the map) you need to provide a "key" attribute. that will help React keep track of things that needs to be updated or rerendered.
O updated your code with those changes in this link, if you wanna see it working:
https://codesandbox.io/s/reverent-browser-zkum2
Good luck and hope you are enjoying React Hooks :)