I want to add an array of <TokenFeed/> functional components to the <Feeds/> component. The problem is that when I do, the onClick() event that passes some text to my <App/> doesn't work as expected. The array version of <TokenFeed/> when clicked, will replace my <input/> text rather than appending to the end of it.
On the contrary, when I add a copy of <TokenFeed/> in the return of <Feeds/>,
and click the button, it works fine.
How can I fix this?
Demo
import React, { useState } from "react";
import { Feeds } from "./Feeds";
export default function App() {
const [inputValue, setInputValue] = useState("");
const [showFeeds, setShowFeeds] = useState();
function createFeeds(e) {
if (e._reactName === "onClick" && showFeeds === undefined) {
setShowFeeds(
<Feeds
value={(val) => {
setInputValue(inputValue + val);
createFeeds("");
}}
/>
);
} else {
setShowFeeds(undefined);
}
}
return (
<>
<input
type="text"
placeholder="Message"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
></input>
<button onClick={(e) => createFeeds(e)}>Create Feeds</button>
{showFeeds}
</>
);
}
import React from "react";
import { TokenFeed } from "./TokenFeed";
let tokenFeedArr = [];
export const Feeds = (props) => {
if (tokenFeedArr.length === 0) {
tokenFeedArr.push(
<TokenFeed
key={"1"}
onClick={() => props.value("Array")}
tokenName={"Array"}
tokenPrice={"Test"}
/>
);
}
return (
<section>
{/* This doesn't work */}
{tokenFeedArr}
{/* This does work */}
<TokenFeed
key={"2"}
onClick={() => props.value("Direct")}
tokenName={"Direct"}
tokenPrice={"Test"}
/>
</section>
);
};
import React from "react";
export const TokenFeed = (props) => {
return (
<section
onClick={() => props.onClick()}
style={{ backgroundColor: "yellow", width: "10%", textAlign: "center" }}
>
<h1>{props.tokenName}</h1>
<p>{props.tokenPrice}</p>
</section>
);
};
You need to declare let tokenFeedArr = []; inside the Feeds component.
Instead of:
let tokenFeedArr = [];
export const Feeds = (props) => {
if (tokenFeedArr.length === 0) {
tokenFeedArr.push(
<TokenFeed
key={"1"}
onClick={() => props.value("Array")}
tokenName={"Array"}
tokenPrice={"Test"}
/>
);
}
...
Try this:
export const Feeds = (props) => {
const tokenFeedArr = [];
if (tokenFeedArr.length === 0) {
tokenFeedArr.push(
<TokenFeed
key={"1"}
onClick={() => props.value("Array")}
tokenName={"Array"}
tokenPrice={"Test"}
/>
);
}
...
The reason the tokenFeedArr that is declared outside the Feeds component doesn't work has to do with JavaScript closures. Specifically the issue lies within the value() function that's inside instances of TokenFeed inside the tokenFeedArr.
The value() function that is passed to an instance of TokenFeed inside of the tokenFeedArr only has access to inputValue as it was when the component was mounted (which is an empty string). It's not connected to inputValue via React state, because it's outside of the scope of the exported component. This is true even though the TokenFeed components are pushed to tokenFeedArr inside the Feeds component. tokenFeedArr is still declared outside Feeds. The setInputValue function works, because of how the useState hook works, but the inputValue variable is just a variable and is subject to JavaScript closures/hoisting, which causes it to retain its original value.
Related
I have a basic increment app in react with the following code. Passing in the handleClick function into the first button works fine, however passing it to a child component IcrementButton that returns the exact same button gives the error:
React: Expected `onClick` listener to be a function, instead got a value of `object. Why is it working for the first button but not the child component button and how can it be fixed? thanks in advance.
import { useState } from "react";
import "./styles.css";
const IncrementButton = (handleClick) => {
return (
<button onClick={handleClick}>+</button>
)
}
export default function App() {
const [num, setNum] = useState(0)
const handleClick = (e) => {
e.preventDefault()
console.log('clicked')
setNum(prev => prev += 1)
}
return (
<div className="App">
<div>{num}</div>
<button onClick={handleClick}>+</button>
<IncrementButton handleClick={handleClick} />
</div>
);
}
Since the IncrementButton is a custom component all props passed to it is sent as an object and to access it you need to use props.property. There are 2 ways to get your code to work.
Use props.property
const IncrementButton = (props) => {
return (
<button onClick={props.handleClick}>+</button>
)
}
Destructure the props object
const IncrementButton = ({handleClick}) => {
return (
<button onClick={handleClick}>+</button>
)
}
You didn't destructure handle click. Try the code below
const IncrementButton = ({handleClick}) => {
return (
<button onClick={handleClick}>+</button>
)
}
That does not work because React passes every props on a Component as a single Object.
You have two ways to get handleClick function reference.
Destructure props (es6)
const IncrementButton = ({handleClick}) => {
return (
<button onClick={handleClick}>+</button>
)
}
Use props.propertyName
const IncrementButton = (props) => {
return (
<button onClick={props.handleClick}>+</button>
)
}
Same as title! Why can not retrieve the state value when passing as (props) but can when passing ({props})?
Code example:
import React, { useState } from 'react'
const Header = (props) => {
return (
<div>
<h1>{props.text}</h1>
</div>
)
}
const Button = (props) => (
<button onClick={props.handleClick}>
{props.text}
</button>
)
const Statistic = props => ( <div><h2>{props.text}</h2></div>)
const Statistics = ({props}) =>{
const total = props.good + props.neutral + props.bad
console.log("total", total)
return(
<div></div>
)
}
const App = () => {
// save clicks of each button to its own state
const [clicks, setClicks] = useState({
good: 0, neutral: 0, bad: 0
})
const setClickToValue= newValue => {
setClicks(newValue)
}
return (
<div>
<Header text="Give feedback" />
<Button handleClick={ () => setClickToValue(clicks.good + 1) } text="Good" />
<Button handleClick={ () => setClickToValue(clicks.neutral + 1) } text="Neutral" />
<Button handleClick={ () => setClickToValue(clicks.bad + 1) } text="Bad" />
<Statistic text="Statistics" />
<Statistics props={clicks} />
</div>
)
}
export default App
I lost an hour to figure out that passing as ({props}) was the correct way. But I still do not understand whats happening, could someone kindly elaborate?
Thanks folks!
the reason is because you have a prop call props.
<Statistics props={clicks} />
so the son component gets this as props
props = {
props: clicks
}
so when you use it like this
const comp = props => {
props.good
}
it breaks or so because props only have prop call props
what you have to do is some like
<Statistics {...clicks} />
now at yow Statistics component you'll get them props correct
const Statistics = props => {
console.log(props) // {good: 0, neutral: 0, bad: 0}
}
Thats because react functional component has default argument called props that is an object.
To access the the props object we can access by . or [] notation for eg., props.name
and javascript also as functionality called object destruction
suppose i have object
let obj = {a:123,b:1243}
// i can directly access the object property by destructuring
let {a} = obj;
console.log(a)
//output 123
take look at destructuring --->
I use weather API for my application. The idea is to get the data from the API once as an array and pass it down for further processing. My App.js file looks like this:
import { useState, useEffect } from "react";
import Search from "./components/Search";
import axios from "axios";
function App() {
const [countries, setCountries] = useState([]);
useEffect(() => {
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
setCountries(response.data);
});
}, []);
return (
<div>
<Search countriesList={countries} />
</div>
);
}
export default App;
The Search component includes a text input field, based on which the incoming array would be filtered and dynamically displayed. However, a function responsible for filtering is not invoked.
Here are the contents of the search component:
import { useState } from "react";
import Country from "./Country";
const Search = ({ countriesList }) => {
const [name, setName] = useState("");
console.log(countriesList);
console.log("countries received");
const filterCountries = (singleCountry, nameFilter) => {
console.log("hello");
console.log(singleCountry);
if (singleCountry.name.toLowerCase().includes(nameFilter.toLowerCase())) {
return singleCountry;
}
};
const countryRender = (showButtonCondition, showWeatherCondition) => {
return (
<div>
{countriesList
.filter((country) => filterCountries(country, name))
.map((filteredCountry) => (
<Country
key={filteredCountry.alpha3Code}
showButton={showButtonCondition}
showWeather={showWeatherCondition}
countryId={filteredCountry.alpha3Code}
countryName={filteredCountry.name}
countryCapital={filteredCountry.capital}
countryPopulation={filteredCountry.population}
countryLanguages={filteredCountry.languages}
countryFlag={filteredCountry.flag}
/>
))}
</div>
);
};
const nameChangeHandler = (event) => {
console.log(event.target.value);
setName(event.target.value);
};
return (
<div>
search: <input value={name} onChange={nameChangeHandler} />
<div>
{countriesList.length > 10 || countriesList.length === 0 ? (
<div>Too many countres, specify another filter</div>
) : (
<></>
)}
{countriesList.length === 1 ? countryRender(false, true) : <></>}
{countriesList.length > 1 && countriesList.length < 10 ? (
countryRender(true, false)
) : (
<></>
)}
</div>
</div>
);
};
export default Search;
I guess that the problem is the changing state of name (user input) that causes the whole Search component to re-render and get the full array anew, but how to overcome it? The React.memo() method doesn't seem to be applicable here, as the documentation states clearly that it shouldn't be used for preventing a component from re-rendering.
You are never actually calling countryRender(true, false). It only gets called when countriesList.length > 1 && countriesList.length < 10 but its length is 250.
React throws the following error when I am trying to render different components
Warning: React has detected a change in the order of Hooks called by GenericDialog. This will lead to bugs and errors if not fixed.
Previous render
Next render
useRef
useRef
useState
useState
useState
useState
useState
useState
useState
useState
useState
useState
useContext
useState
I do agree this would be inappropriate when I would be rendering the same component each time but with different order of hooks. What I am trying to achieve is render a different component each time so it is quite obvious the order of hooks won't be identical.
I have created this GenericDialog component which renders a multistep dialog.
import React, { useRef, useState, useEffect } from 'react';
import { DialogFooterNavigation } from './DialogFooterNavigation';
import { Dialog } from '../../../../Dialog';
import { Subheader } from '../../../../Subheader';
import { Loading } from '../../../../Loading';
export interface FooterConfiguration {
onContinue?: () => Promise<boolean | void>;
isContinueDisabled?: boolean;
continueLabel?: string;
isBackHidden?: boolean;
isCancelHidden?: boolean;
}
export interface HeaderConfiguration {
subheader?: string;
}
export interface DialogStepProps {
setHeaderConfiguration: (config: HeaderConfiguration) => void;
setFooterConfiguration: (config: FooterConfiguration) => void;
}
export type DialogStep = (props: DialogStepProps) => JSX.Element;
interface GenericDialogProps {
isShown: boolean;
hideDialog: () => void;
steps: DialogStep[];
header: string;
}
export const GenericDialog = ({
isShown,
hideDialog,
steps,
header,
}: GenericDialogProps) => {
const buttonRef = useRef(null);
const [step, setStep] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [headerConfiguration, setHeaderConfiguration] = useState<HeaderConfiguration | undefined>(
undefined,
);
const [footerConfiguration, setFooterConfiguration] = useState<FooterConfiguration | undefined>(
undefined,
);
const [loadingMessage, setLoadingMessage] = useState<string>('');
const dialogBody = steps[step]({
setHeaderConfiguration,
setFooterConfiguration,
});
const nextStep = () => {
if (step < steps.length - 1) {
setStep(step + 1);
}
};
const prevStep = () => step > 0 && setStep(step -1);
const isBackPossible = step > 0;
const onBack = () => (isBackPossible || footerConfiguration?.isBackHidden ? undefined : prevStep);
const onContinue = async () => {
setIsLoading(true);
const result = await footerConfiguration?.onContinue?.call(undefined);
setIsLoading(false);
if (result === false) {
return;
}
nextStep();
};
return (
<Dialog isShown={isShown} onHide={hideDialog}>
<div>
{header}
{headerConfiguration?.subheader && (
<Subheader>{headerConfiguration.subheader}</Subheader>
)}
</div>
{isLoading && loadingMessage ? <Loading msg={loadingMessage} /> : dialogBody}
{!isLoading && (
<DialogFooterNavigation
onBack={isBackPossible ? onBack : undefined}
onContinue={onContinue}
isContinueDisabled={footerConfiguration?.isContinueDisabled}
/>
)}
</Dialog>
);
};
const FirstStep = (props: DialogStepProps) => {
// Here I need useContext
const { id, name } = useCustomerContext();
useEffect(() => {
props.setFooterConfiguration({
isContinueDisabled: !id || !name,
})
}, [id, name]);
return (
<>
<div>ID: {id}</div>
<div>Name: {name}</div>
</>
);
};
const SecondStep = (props: DialogStepProps) => {
// Here I don't need useContext but I do need useState
const [inputValue, setInputValue] = useState({});
useEffect(() => {
props.setFooterConfiguration({
isContinueDisabled: !inputValue,
});
}, [inputValue]);
return <input value={inputValue} onChange={(event) => setInputValue(event.target.value)} />;
}
const MyDialogExample = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const steps: DialogStep[] = [
FirstStep,
SecondStep,
];
return (
<>
<button onClick={() => setIsDialogOpen(true)}>Open Dialog</button>
<GenericDialog
isShown={isDialogOpen}
hideDialog={() => setIsDialogOpen(false)}
steps={steps}
header="Dialog example"
/>
</>
);
};
The problem is here:
const dialogBody = steps[step]({
setHeaderConfiguration,
setFooterConfiguration,
});
Try changing it to something like this:
const DialogBody = steps[step];
And then, in your return statement:
{isLoading && loadingMessage ? <Loading msg={loadingMessage} /> : <DialogBody setHeaderConfiguration={setHeaderConfiguration} setFooterConfiguration={setFooterConfiguration} />}
Please note that it can be done differently, like:
const DialogBody = steps[step];
const dialogBody = <DialogBody setHeaderConfiguration={setHeaderConfiguration} setFooterConfiguration={setFooterConfiguration} />;
And keeping your return statement unaltered.
Explanation
Your code isn't entirely wrong though. When working with functional components, there is a subtle difference between an actual component, a hook and a simple function that returns an instantiated component based on some logic. The problem is that you are mixing those three.
You can't manually instantiate a component by calling its corresponding function (just like you can't instantiate a class component by using the new operator). Either you use JSX (like <DialogBody />) or directly use React inner methods (Like React.createElement()). Both alternatives are different from just doing dialogBody(). For example, if you see the compiled JSX code you will note that <DialogBody /> compiles to code that uses React.createElement() and the latter returns a real React element instance containing many special properties and methods.
dialogBody() would work if its only goal was to return an instantiated element (Using one of the methods above) based on some logic. This implies not using any hook along with some other constraints.
Instead, your dialogBody 'function' contains hooks and it acts as a custom hook itself. This is why React complains about hooks execution order. You are executing hooks conditionally.
I have this structure
component 1
import React, { useState } from 'react'
export default function Component1() {
return (
<div>
<button onClick={handleChange}></button>
</div>
)
}
component 2
import React, { useState } from 'react'
export default function Component2() {
return (
<div>
<button onClick={handleChange}></button>
</div>
)
}
and the parent
import React from 'react'
export default function Parent() {
return (
<div>
<Component1 />
<Component2 />
</div>
)
}
The question is, how can I toggle visibility between the two, without having a button in the parent. Just the buttons inside each component. - The Component1 should be visible by default and when you press the button in Component1 it will hide it and show Component2 and vice-versa.
I've tried using useState hook on the Component1 button, but I'm not sure how to export the state and add it to the parent component.
const [showMini, setShowMini] = useState(false);
const handleChange = () => {
setShowMini(true);
}
Is this possible? or it's possible just with a button in the parent that control the two?
Thanks
Try this:
import React from 'react'
export default function Parent() {
const[show,setShow]=useState(false);
const handleChange=()=>{
setShow(!show);
}
return (
<div>
{show ? <Component2 handleChange={handleChange}/> : <Component1 handleChange={handleChange}/>}
</div>
)
}
and inside Component1 have this:
import React, { useState } from 'react'
export default function Component1({handleChange}) {
return (
<div>
<button onClick={handleChange}></button>
</div>
)
}
Similarly do it for Component2
You can do with state value and pass handleChange function ad props in the child component and in click on the button in child component call handleChange method under parent component and show hide based on state value.
import React from 'react'
const [showChild, setshowChild] = useState(false);
const handleChange = () => {
setshowChild(!showChild);
}
export default function Parent() {
return (
<div>
{showChild ? <Component2 handleChange = {handleChange}/> : <Component1 handleChange= {handleChange} />}
</div>
)
}
You can manage the state in the parent and pass down a handler to the children
import React, { useState } from 'react'
const [currentView, setCurrentView] = useState('component1')
const changeCurrentView = (view) => setCurrentView(view)
const renderViews = () => {
switch(currentView) {
case 'component1':
return <Component1 changeCurrentView={changeCurrentView} />
case 'component2':
return <Component2 changeCurrentView={changeCurrentView} />
default:
return <Component1 changeCurrentView={changeCurrentView} />
}
}
export default function Parent() {
return (
<div>
{renderViews()}
</div>
)
}
Other components
import React from 'react'
export default function Component1({ changeCurrentView }) {
return (
<div>
<button onClick={() => changeCurrentView('component1')}></button>
</div>
)
}
export default function Component2({ changeCurrentView }) {
return (
<div>
<button onClick={() => changeCurrentView('component2')}></button>
</div>
)
}
Your parent component should keep track of the state:
import React, {useState} from 'react'
export default function Parent() {
const [showChild, setShowChild] = useState(1);
const showNextChild = () => {
setShowChild( showChild === 1 ? 2 : 1 ); // set to 2 if already 1, else to 1
}
return (
<div>
{ showChild === 1 && <Component1 handleChange={showNextChild} /> }
{ showChild === 2 && <Component2 handleChange={showNextChild} /> }
</div>
)
}
A few notes:
Your components are identical, so the duplication is unnecessary, but I assume the example is just contrived.
This assumes toggling 2 components back and forth. If you have more than 2 components you are "looping" through, you can instead increment the previous showChild state and then reset it to 0 if higher than the # of components you have.
The syntax you see, showChild === 1 && <Component1 ... uses the behavior of the && operator which actually returns the 2nd item it is evaluating if both are true. In other words, const isTrue = foo && bar; sets isTrue to bar, not true as you might expect. (You know, however, that bar is "truthy" in this case, so isTrue still works in future if statements and such.) The component is always truthy, so the effect is that the component is returned if the first part is true, otherwise it is not. It's a good trick for conditionally showing components.
Try this. You can send information from child to parent with functions passed as a prop.
Parent Component:
const Parent = () => {
const [show, setShow] = useState(true);
const toggleVisibility = () => {
setShow(!show);
};
return (
<div>
{show ? (
<Child1 toggle={toggleVisibility}></Child1>
) : (
<Child2 toggle={toggleVisibility}></Child2>
)}
</div>
);
};
Child 1
const Child1 = (props) => {
const { toggle } = props;
return (
<div style={{ width: '100px', height: '100px' }}>
<button onClick={toggle}>Child 1's button</button>
</div>
);
};
Child 2
const Child2 = (props) => {
const { toggle } = props;
return (
<div style={{ width: '100px', height: '100px' }}>
<button onClick={toggle}>Child 2's button</button>
</div>
);
};