Map of refs, current is always null - javascript

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.

Related

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

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>

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().

removing object in an array based on one of it's properties

I am trying to manipulate an array of components inside the state and I want to be able to remove each component based on it's id property. I cant figure out how to target the component when I pass the id dynamically each time I create the component.
constructor(props) {
super(props);
this.state = {
counters: 0, // counters count
countersArr: []
}}
render() {
let countersArrCop = this.state.countersArr;
let counterIndex;
onAddCounterHandler = () => {
const id = new Date().getTime()
console.log(this.state.countersArr.length)
console.log(counterIndex)
countersArrCop.push( // push new counter to the counters holder
<Counter
style={styles.countersContainer}
key={id}
id={id}
remove={() => removeCounterHandler(id)}
/>
)
this.setState({
countersArr: countersArrCop,
})
console.log(id)
}
// remove counter
removeCounterHandler = (id) => {
console.log(id)
const countersArr = this.state.countersArr.slice(); // Local copy to manipulate
this.setState({ countersArr: countersArr.filter(counter => counter.id !== id) });
}
return (
<View style={styles.container}>
<View style={styles.addBtnContainer}>
<Button
onPress={onAddCounterHandler}
title={'add Counter'}
/>
</View>
{
/* return each individual counter from the array by using 'map' */
}
<View style={styles.container}> {
this.state.countersArr.map((counter) => {
return counter;
})
}
</View>
</View>
);
UPDATED THE CODE *** id === undefined.. why?
first of all putting logical codes in your render() is highly discouraged. render should only render, any other task should be where they should be. Note that all the code in your render block gets called every time the component re-renders. So those handlers should be declared as methods within your component class with proper hooks from your render like maybe onClick() and don't forget to bind your this context like this to keep your this reference to your component class inside your methods:
<Button onClick={this.onAddCounterHandler.bind(this)} />
second, you're bloating your state with an array of components when you can just store the ids in an array:
onAddCounterHandler() {
const { countersArr } = this.state
const id = new Date().getTime()
this.setState({
countersArr: [...countersArr, id],
})
}
then just map that array and return the component like this:
{ this.state.countersArr.map(id=>
<Counter
style={styles.countersContainer}
key={id}
id={id}
remove={this.removeCounterHandler.bind(this, id)}
/>
) }
note that in jsx, this statement should be enclosed in braces {}
then on your filtering function:
removeCounterHandler(filteredID) {
const { countersArr } = this.state
const filtered = countersArr.filter(id=>id!==filteredID)
this.setState({ countersArr: filtered});
}
Lastly, NEVER ever put setState in your render
try it and let me know how it goes :)
You can do a function like this one:
removeCounterHandler (id) {
const countersArr = this.state.countersArr.slice(); // Local copy to manipulate
this.setState({ countersArr: countersArr.filter(counter => counter.id !== id) });
}

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 :)

How do I iterate over an array with a map function, infinitely?

I am building a React JS application.
So, I want to print something over and over from an array, but it has only two elements. I am using a custom package called 'Typist' that enables me to give a 'Typing' kind of animation with whatever I type.
I am basically trying to type 'Hi', erase it and then type 'Ola' and then erase it and then start with 'Hi' again and keep repeating this pattern.
Here's what I have right now:
let greetings=["Hi","Ola"];
render() {
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{
greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
})
}
</Typist>
P.S I did find a way to do it a few times,still not infinite, by doing this:
let greetings=["Hi","Ola"];
var actualTyping= greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
});
var rows=[];
for(var i=0;i<10;i++){
rows.push(actualTyping)
}
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{rows}
</Typist>
</div>
);
You can use Typist's onTypingDone property to restart the animation. Pass the text array via state to Typist. When the typing is done, clear the state, which will remove the rendered Typist, then restore the original text, and it will be typed again (sandbox).
Note: setState is asynchronous, and it batches updates together or defers them to a later time. In this case we want to clear the text (set null), and only after the view is updated repopulate the text. To do so, we can use the 2nd param of setState, a callback that is fired only after the update (null) has been applied.
const greetings = ["Hi", "Ola"];
class ContType extends React.Component {
constructor(props) {
super(props);
this.state = {
text: props.text
};
}
onTypingDone = () => {
this.setState(
{
text: null
},
() =>
// run this callback after the state updates
this.setState({
text: this.props.text
})
);
};
render() {
const { text } = this.state;
return (
text && (
<Typist onTypingDone={this.onTypingDone}>
<Typist.Delay ms={1000} />
{text.map((i, index) => {
return (
<div key={index}>
<h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</div>
);
})}
</Typist>
)
);
}
}
render(<ContType text={greetings} />, document.getElementById("root"));
Better and simple solution would be creating a constant integer array of your choice and then mapping carried out using the value specified for integer.
const a = [1...10000]
let greetings = ["hi", "hello"]
render(){
return(
a.map( i => {
<h1>greeting[0] - greeting[1]</h1>
})
)
}
And always, keep in mind that infinite loop cause react engine to break down. Good practice is to specify an integer value for mapping for such cases.

Categories

Resources