jdperos
jdperos

Reputation: 115

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.

Upvotes: 0

Views: 672

Answers (1)

shamsup
shamsup

Reputation: 2032

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.

Upvotes: 1

Related Questions