How can I render an array of components in react without them unmounting? - javascript

I have an array of React components that receive props from a map function, however the issue is that the components are mounted and unmounted on any state update. This is not an issue with array keys.
Please see codesandbox link.
const example = () => {
const components = [
(props: any) => (
<LandingFirstStep
eventImage={eventImage}
safeAreaPadding={safeAreaPadding}
isActive={props.isActive}
onClick={progressToNextIndex}
/>
),
(props: any) => (
<CameraOnboarding
safeAreaPadding={safeAreaPadding}
circleSize={circleSize}
isActive={props.isActive}
onNextClick={progressToNextIndex}
/>
),
];
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={`component-key-${index}`} isActive={isActive} />;
})}
</div>
)
}
If I render them outside of the component.map like so the follow, the component persists on any state change.
<Comp1 isActive={x === y}
<Comp2 isActive={x === y}
Would love to know what I'm doing wrong here as I am baffled.
Please take a look at this Codesandbox.
I believe I am doing something wrong when declaring the array of functions that return components, as you can see, ComponentOne is re-rendered when the button is pressed, but component two is not.

You should take a look at the key property in React. It helps React to identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity

I think there are two problems:
To get React to reuse them efficiently, you need to add a key property to them:
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={anAppropriateKeyValue} isActive={isActive} />;
})}
</div>
);
Don't just use index for key unless the order of the list never changes (but it's fine if the list is static, as it appears to be in your question). That might mean you need to change your array to an array of objects with keys and components. From the docs linked above:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
I suspect you're recreating the example array every time. That means that the functions you're creating in the array initializer are recreated each time, which means to React they're not the same component function as the previous render. Instead, make those functions stable. There are a couple of ways to do that, but for instance you can just directly use your LandingFirstStep and CameraOnboarding components in the map callback.
const components = [
{
Comp: LandingFirstStep,
props: {
// Any props for this component other than `isActive`...
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
// Any props for this component other than `isActive`...
onNextClick: progressToNextIndex
}
},
];
then in the map:
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
There are other ways to handle it, such as via useMemo or useCallback, but to me this is the simple way — and it gives you a place to put a meaningful key if you need one rather than using index.
Here's an example handling both of those things and showing when the components mount/unmount; as you can see, they no longer unmount/mount when the index changes:
const {useState, useEffect, useCallback} = React;
function LandingFirstStep({isActive, onClick}) {
useEffect(() => {
console.log(`LandingFirstStep mounted`);
return () => {
console.log(`LandingFirstStep unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onClick}>LoadingFirstStep, isActive = {String(isActive)}</div>;
}
function CameraOnboarding({isActive, onNextClick}) {
useEffect(() => {
console.log(`CameraOnboarding mounted`);
return () => {
console.log(`CameraOnboarding unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onNextClick}>CameraOnboarding, isActive = {String(isActive)}</div>;
}
const Example = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const progressToNextIndex = useCallback(() => {
setCurrentIndex(i => (i + 1) % components.length);
});
const components = [
{
Comp: LandingFirstStep,
props: {
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
onNextClick: progressToNextIndex
}
},
];
return (
<div>
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById("root"));
.active {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Related

Problems using useRef / useImperativeHandle in mapping components

I have a dashboard with different components. Everything is working with a separate start-button on each component, now I need to have a common start-button, and for accessing the children's subfunctions from a parent, I understand that in React you should use the useRef.(but its perhaps not correct, but I'm struggling to see another way). I would like to have the flexibility to choose which component to start from this "overall start-button"
I have a component list that i map through shown below.
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
/>
)
This works fine, but I would like, as mentioned, to access a function called something like "buttonclick" in each of the children, so I tested this with a pressure-gauge component
The function "exposed" via the forwardRef and the useImparativeHandle
const ShowRadialGauge = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
buttonclick() {
setStart(!start);
},
}));
)
then in my dashboard I changed to :
const gaugepressure = useRef();
return(
<div>
<Button onClick={() => gaugepressure.current.buttonclick()}>
Start processing
</Button>
<ShowRadialGauge ref={gaugepressure} />
<div>
)
This works fine if I use the useRef from the dashboard and instead of mapping over the components, I add them manually.
I understand the useRef is not a props, but its almost what I want. I want to do something like this:
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
**ref={item.ref}**
/>
)
where the ref could be a part of my component array (as below) or a separate array.
export const ComponentsList = [
{
name: "Radial gauge",
text: "showradialgauge",
component: ShowRadialGauge,
ref: "gaugepressure",
},
{
name: "Heatmap",
text: "heatmap",
component: Heatmap,
ref: "heatmapstart",
},
]
Anyone have any suggestions, or perhaps do it another way?
You are on the right track with a React ref in the parent to attach to a single child component. If you are mapping to multiple children though you'll need an array of React refs, one for each mapped child, and in the button handler in the parent you will iterate the array of refs to call the exposed imperative handle from each.
Example:
Parent
// Ref to hold all the component refs
const gaugesRef = React.useRef([]);
// set the ref's current value to be an array of mapped refs
// new refs to be created as needed
gaugesRef.current = componentsList.map(
(_, i) => gaugesRef.current[i] ?? React.createRef()
);
const toggleAll = () => {
// Iterate the array of refs and invoke the exposed handle
gaugesRef.current.forEach((gauge) => gauge.current.toggleStart());
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Gauges
</button>
{componentsList.map(({ name, component: Component, ...props }, i) => (
<Component
key={name}
ref={gaugesRef.current[i]}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = React.forwardRef(({ name }, ref) => {
const [start, setStart] = React.useState(false);
const toggleStart = () => setStart((start) => !start);
React.useImperativeHandle(ref, () => ({
toggleStart
}));
return (....);
});
The more correct/React way to accomplish this however is to lift the state up to the parent component and pass the state and handlers down to these components.
Parent
const [gaugeStarts, setGaugeStarts] = React.useState(
componentsList.map(() => false)
);
const toggleAll = () => {
setGaugeStarts((gaugeStarts) => gaugeStarts.map((start) => !start));
};
const toggleStart = (index) => {
setGaugeStarts((gaugeStarts) =>
gaugeStarts.map((start, i) => (i === index ? !start : start))
);
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Guages
</button>
{componentsList.map(({ name, component: Component, ...props },, i) => (
<Component
key={name}
start={gaugeStarts[i]}
toggleStart={() => toggleStart(i)}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = ({ name, start, toggleStart }) => {
return (
<>
...
<button type="button" onClick={toggleStart}>
Toggle Start
</button>
</>
);
};
#Drew Reese
Thx Drew,
you are off course correct. I'm new to React, and I'm trying to wrap my head around this "state handling".
I tested your suggestion, but as you say, its not very "React'ish", so I lifted the state from the children up to the parent.
In the parent:
const [componentstate, setComponentstate] = useState([
{ id:1, name: "pressuregauge", start: false},
{ id:2, name: "motormap", start: false },
{ id:3, name: "heatmapstart", start: false},
]);
then in the component ShowRadialGauge, I did like this:
const ShowRadialGauge = ({ props, componentstate })
and if we need to keep the button in each component, I have the id in the componentstate object that is desctructured, so I can send that back.
.
First of all, why do you need refs to handle click when you can access it via onClick. The most common use case for refs in React is to reference a DOM element or store value that is persist between renders
My suggestion are these
First, try to make it simple by passing a function and then trigger it via onClick
Second if you really want to learn how to use imperativeHandle you can reference this video https://www.youtube.com/watch?v=zpEyAOkytkU.

React render previous values during conditional fade out animation

I am using react-transition-group to fade out various components. I'm converting simple conditional renders such as:
{valueToDisplay && <MyComponent {...valueToDisplay} />}
To transitions such as:
<CSSTransition
in={!!valueToDisplay}
unmountOnExit
classNames="fade"
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
<MyComponent {...valueToDisplay} />
</CSSTransition>
The issue I'm running into is when the "in" property of the transition becomes false, and the exit transition is running, the child component will now have null prop values. This can cause exceptions or cause the child content to flash and change during the exit. What I would like to see instead is that during the exit transition, the content will remain unchanged.
The first solution I came up with was to make child components to cache previous values of their props, and then use those previous values when their props become null. However I don't like this solution because it forces all components which will be transitioned to introduce new and confusing internal logic.
The second attempt I made was to create a wrapper component which cached the previous value of props.children, and whenever "in" becomes false, renders the cached children instead. This essentially "freezes" the children as they were the last time in was true, and they don't change during the exit transition. (If this solution is the general practice, is there a better way of doing this, perhaps with the useMemo hook?)
For such a common use case of fading content out, this solution doesn't seem very intuitive. I can't help but feeling I'm going about this the wrong way. I can't really find any examples of having to cache/memoize content to keep it displaying during fade outs. It seems like something somewhere has to remember the values to display when performing the exit transition. What am I missing?
Here is a minimal example and working example:
import { useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CSSTransition } from 'react-transition-group';
const Pet = ({ type, age }) => {
return (
<div>
Your pet {type || 'null'} is age {age || 'null'}
</div>
);
};
const Fade = ({ show, children }) => {
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
in={show}
unmountOnExit
classNames="fade"
addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
>
<span ref={nodeRef}>
{children}
</span>
</CSSTransition>
);
};
const FadeWithMemo = ({ show, children }) => {
const previousChildren = useRef();
useEffect(() => {
previousChildren.current = show ? children : null;
}, [show, children]);
return (
<Fade show={show}>
{show ? children : previousChildren.current}
</Fade>
);
};
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => setCurrentPet(getPet())}>Set</button>
<button onClick={() => setCurrentPet(null)}>Clear</button>
<div>
The Problem:
<Fade show={!!currentPet}>
<Pet {...currentPet} />
</Fade>
</div>
<div>
Potential Fix:
<FadeWithMemo show={!!currentPet}>
<Pet {...currentPet} />
</FadeWithMemo>
</div>
</>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<Example />);
You can detach the visible condition from the pet state so that you have more granular control over whether something is visible and what is actually being displayed.
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const [showPet, setShowPet] = useState(false);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => {
setCurrentPet(getPet());
setShowPet(true);
}}>Set</button>
<button onClick={() => setShowPet(false)}>Clear</button>
<div>
<Fade show={showPet}>
<Pet {...currentPet} />
</Fade>
</div>
</>
);
};
or you can have the visible be part of the pet state and only set that part to false.

Map of refs, current is always null

I'm creating a host of a bunch of pages, and those pages are created dynamically. Each page has a function that I'd like to call at a specific time, but when trying to access a ref for the page, the current is always null.
export default class QuizViewPager extends React.Component<QuizViewPagerProps, QuizViewPagerState> {
quizDeck: Deck | undefined;
quizRefMap: Map<number, React.RefObject<Quiz>>;
quizzes: JSX.Element[] = [];
viewPager: React.RefObject<ViewPager>;
constructor(props: QuizViewPagerProps) {
super(props);
this.quizRefMap = new Map<number, React.RefObject<Quiz>>();
this.viewPager = React.createRef<ViewPager>();
this.state = {
currentPage: 0,
}
for (let i = 0; i < this.quizDeck!.litems.length; i++) {
this.addQuiz(i);
}
}
setQuizPage = (page: number) => {
this.viewPager.current?.setPage(page);
this.setState({ currentPage: page })
this.quizRefMap.get(page)?.current?.focusInput();
}
addQuiz(page: number) {
const entry = this.quizDeck!.litems[page];
var ref: React.RefObject<Quiz> = React.createRef<Quiz>();
this.quizRefMap.set(page, ref);
this.quizzes.push(
<Quiz
key={page}
litem={entry}
index={page}
ref={ref}
pagerFocusIndex={this.state.currentPage}
pagerLength={this.quizDeck?.litems.length!}
setQuizPage={this.setQuizPage}
navigation={this.props.navigation}
quizType={this.props.route.params.quizType}
quizManager={this.props.route.params.quizType === EQuizType.Lesson ? GlobalVars.lessonQuizManager : GlobalVars.reviewQuizManager}
/>
)
}
render() {
return (
<View style={{ flex: 1 }}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={this.viewPager}
scrollEnabled={false}
>
{this.quizzes}
</ViewPager>
</View >
);
}
};
You can see in addQuiz() I am creating a ref, pushing it into my map, and passing that ref into the Quiz component. However, when attempting to access any of the refs in setQuizPage(), the Map is full of refs with null current properties.
To sum it up, the ViewPager library being used isn't actually rendering the children you are passing it.
If we look at the source of ViewPager (react-native-viewpager), we will see children={childrenWithOverriddenStyle(this.props.children)} (line 182). If we dig into the childrenWithOverriddenStyle method, we will see that it is actually "cloning" the children being passed in via React.createElement.
It is relatively easy to test whether or not the ref passed to these components will be preserved by creating a little demo:
const logRef = (element) => {
console.log("logRef", element);
};
const ChildrenCreator = (props) => {
return (
<div>
{props.children}
{React.Children.map(props.children, (child) => {
console.log("creating new", child);
let newProps = {
...child.props,
created: "true"
};
return React.createElement(child.type, newProps);
})}
</div>
);
};
const App = () => {
return (
<div className="App">
<ChildrenCreator>
<h1 ref={logRef}>Hello World</h1>
<p>It's a nice day!</p>
</ChildrenCreator>
</div>
);
}
ReactDOM.render(<App />, '#app');
(codesandbox)
If we look at the console when running this, we will be able to see that the output from logRef only appears for the first, uncopied h1 tag, and not the 2nd one that was copied.
While this doesn't fix the problem, this at least answers the question of why the refs are null in your Map. It actually may be worth creating an issue for the library in order to swap it to React.cloneElement, since cloneElement will preserve the ref.

React HOC working on some but not other components

I'm using a HOC component to bind an action to many different types of element, including SVG cells, which, when an onClick is bound normally, it works, but when I use my HOC it returns un-intended results.
Minimally reproducible example: https://codesandbox.io/s/ecstatic-keldysh-3viw0
The HOC component:
export const withReport = Component => ({ children, ...props }) => {
console.log(Component); //this only prints for ListItem elements for some reason
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
console.log('clicked!'); //even this wont work on some.
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}>
{children}
</Component>
);
};
Usage working:
const ListItemWIthReport = withReport(ListItem); //list item from react-mui
{items.map((item, key) => (
<ListItemWithReport report={item.report} key={key} button>
{/* listitem children*/}
</ListItemWithReport>
))}
Usage not working:
const BarWithReport = withReport(Bar); //Bar from recharts
{bars.map((bar, index) => (
<BarWithReport
report={bar.report}
key={index}
dataKey={bar.name}
fill={bar.fill}
/>
))}
The ListItem works 100% as anticipated, however, the bars will not render inside of the BarChart. Similarly, with a PieChart the Cells will actually render, with the correct sizes according to their values, however, props like "fill" do not appear to pass down.
Am I using the HOC incorrectly? I don't see an option other than HOC for the inside of Charts as many types of elements will be considered invalid HTML?
You might be dealing with components that have important static properties that need to be hoisted into the wrapped component or need to have ref forwarding implemented in order for their parent components to handle them. Getting these pieces in place is important, especially when wrapping components where you don't know their internals. That Bar component, for example, does have some static properties. Your HOC is making those disappear.
Here's how you can hoist these static members:
import hoistNonReactStatic from 'hoist-non-react-statics';
export const withReport = Component => {
const EnhancedComponent = props => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}/>
);
};
hoistNonReactStatic(EnhancedComponent, Component);
return EnhancedComponent;
};
Docs on hoisting statics and ref forwarding can be found in this handy guide to HOCs.
There may be some libraries that can take care of all these details for you. One, addhoc, works like this:
import addHOC from 'addhoc';
export const withReport = addHOC(render => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return render({ onClick: handleClick });
});
Of course, if the parent component is checking child components by type explicitly, then you won't be able to use HOCs at all. In fact, it looks like recharts has that issue. Here you can see the chart is defined in terms of child components which are then searched for explicitly by type.
I think your HOC is invalid, because not every wrapper-Component (e.g. HTML element) is basically clickable. Maybe this snipped can clarify what I am trying to say:
const withReport = Component => (props) => {
const handleClick = () => console.log('whatever')
// Careful - your component might not support onClick by default
return <Component onClick={handleClick} {...props} />
// vs.
return <div onClick={handleClick} style={{backgroundColor: 'green'}}>
<Component {...props} />
{props.children}
</div>
}
// Your import from wherever you want
class SomeClass extends React.Component {
render() {
return <span onClick={this.props.onClick}>{this.props.children}</span>
// vs.
return <span style={{backgroundColor: 'red'}}>
{
// Careful - your imported component might not support children by default
this.props.children
}
</span>
}
}
const ReportedListItem = withReport(SomeClass)
ReactDOM.render(<ReportedListItem>
<h2>child</h2>
</ReportedListItem>, mountNode)
You can have the uppers or the unders (separated by vs.) but not crossed. The HOC using the second return (controlled wrapper-Component) is sure more save.
I've used 4 methods successfully to wrap Recharts components.
First Method
Wrap the component in a HOC and use Object.Assign with some overloads. This breaks some animation and difficult to use an active Dot on lines. Recharts grabs some props from components before rendering them. So if the prop isn't passed into the HOC, then it won't render properly.
...
function LineWrapper({
dataOverload,
data,
children,
strokeWidth,
strokeWidthOverload,
isAnimationActive,
dot,
dotOverload,
activeDot,
activeDotOverload,
...rest
}: PropsWithChildren<Props>) {
const defaultDotStroke = 12;
return (
<Line
aria-label="chart-line"
isAnimationActive={false}
strokeWidth={strokeWidthOverload ?? 2}
data={dataOverload?.chartData ?? data}
dot={dotOverload ?? { strokeWidth: defaultDotStroke }}
activeDot={activeDotOverload ?? { strokeWidth: defaultDotStroke + 2 }}
{...rest}
>
{children}
</Line>
);
}
export default renderChartWrapper(Line, LineWrapper, {
activeDot: <Dot r={14} />,
});
const renderChartWrapper = <P extends BP, BP = {}>(
component: React.ComponentType<BP>,
wrapperFC: React.FC<P>,
defaultProps?: Partial<P>
): React.FC<P> => {
Object.assign(wrapperFC, component);
if (defaultProps) {
wrapperFC.defaultProps = wrapperFC.defaultProps ?? {};
Object.assign(wrapperFC.defaultProps, defaultProps);
}
return wrapperFC;
};
Second Method
Use default props to assign values. Any props passed into the HOC will be overridden.
import { XAxisProps } from 'recharts';
import { createStyles } from '#material-ui/core';
import { themeExtensions } from '../../../assets/theme';
const useStyles = createStyles({
tickStyle: {
...themeExtensions.font.graphAxis,
},
});
type Props = XAxisProps;
// There is no actual implementation of XAxis. Recharts render function grabs the props only.
function XAxisWrapper(props: Props) {
return null;
}
XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = {
allowDecimals: true,
hide: false,
orientation: 'bottom',
width: 0,
height: 30,
mirror: false,
xAxisId: 0,
type: 'category',
domain: [0, 'auto'],
padding: { left: 0, right: 0 },
allowDataOverflow: false,
scale: 'auto',
reversed: false,
allowDuplicatedCategory: false,
tick: { style: useStyles.tickStyle },
tickCount: 5,
tickLine: false,
dataKey: 'key',
};
export default XAxisWrapper;
Third Method
I didn't like this so I've worked around it, but you can extend the class.
export default class LineWrapper extends Line {
render(){
return (<Line {...this.props} />
}
}
Fourth Method
I don't have a quick example of this, but I always render the shape or children and provide functions to help. For example, for bar cells I use this:
export default function renderBarCellPattern(cellOptions: CellRenderOptions) {
const { data, fill, match, pattern } = cellOptions;
const id = _uniqueId();
const cells = data.map((d) =>
match(d) ? (
<Cell
key={`cell-${id}`}
strokeWidth={4}
stroke={fill}
fill={`url(#bar-mask-pattern-${id})`}
/>
) : (
<Cell key={`cell-${id}`} strokeWidth={2} fill={fill} />
)
);
return !pattern
? cells
: cells.concat(
<CloneElement<MaskProps>
key={`pattern-${id}`}
element={pattern}
id={`bar-mask-pattern-${id}`}
fill={fill}
/>
);
}
// and
<Bar {...requiredProps}>
{renderBarCellPattern(...cell details)}
</Bar>
CloneElement is just a personal wrapper for Reacts cloneElement().

React functional component - how do I pass refs back to parent when component returns an array of elements?

I have a React component that includes a stateless functional component. The inner component runs Lodash map on an array of values to return an array of p tags.
class Application extends React.Component {
items = [
'first',
'second',
'third',
];
render() {
return <div>
<Paragraphs items={ this.items } />
</div>;
}
}
const renderItem = ( item, index ) => {
return (
<p key={ index }>{ item }</p>
);
};
const Paragraphs = ( { items } ) => _.map( items, renderItem );
ReactDOM.render(<Application />, document.getElementById('root'));
My Application component needs references to these DOM elements, so I'd like to pass back a ref for each of the p tags back to the parent component. Can anyone suggest the best way to do this? All the examples I've found assume the child component is a single element.
Codepen example
Now in React 16.3 you can create refs with React.createRef() and pass them from parent component to child. Here is the docs.
So you can map items in the parent component and extend them with ref property.
I hope this will work for you.
class Application extends React.Component {
items = [
'first',
'second',
'third',
].map(item => ({ item, ref: React.createRef() }))
// you can access refs here: this.items[0].ref
render() {
return <div>
<Paragraphs items={this.items} />
</div>;
}
}
const renderItem = (item, index) => {
return (
<p key={index} ref={item.ref} > {item.item} </p>
);
};
const Paragraphs = ({ items }) => _.map(items, renderItem);
ReactDOM.render(<Application />, document.getElementById('root'));
for starters, you may want to read in here -
https://reactjs.org/docs/refs-and-the-dom.html
https://gist.github.com/gaearon/1a018a023347fe1c2476073330cc5509
And then, if you have grasped the concept of references in react, I have done, some very simple changes( see below ) to the JS file from your codepen's pen.
I am pasting the new pen, with needed specs here - https://codepen.io/anon/pen/wjKvvw
class Application extends React.Component {
items = [
'first',
'second',
'third',
];
item_refs = this.items.map(a=>{}) // making an empty list for references
item_referer = (ele, ind) => { // a callback function to be called in the children where references are to be made
this.item_refs[ind] = ele;
console.log("Referring to", this.items[ind], ele) // a simple logging to show the referencing is done. To see open the console.
}
// passing the item_referer function as the prop (itemReferer) to children
render() {
return <div>
<Paragraphs items={ this.items } itemReferer={ this.item_referer }/>
</div>;
}
}
const renderItem = ( item, index, referToMe ) => {
// referToMe is the callback function to be called while referencing
return (
<p key={ index } ref={(el)=>referToMe(el, index)} >{ item }</p>
);
};
// get the itemReferer prop passed to Paragraphs component and use it
// render the children.
const Paragraphs = ( { items, itemReferer } ) => items.map((item, index )=>{
return renderItem(item, index, itemReferer) // passing to refer to the individual item
})
ReactDOM.render(<Application />, document.getElementById('root'));
Go through the code, if you have any questions, let me know :)

Categories

Resources