1

I'm using react 16.13.1 and react-dom 16.13.1. I create a ref using React.createRef() and attach to a component I defined by myself.And then I want to use a method that I defined in that component, but it does not work because .current is null.Here's my code.

class SomeComponent {
  //ref
  picturesRef = React.createRef();
  richTextRef = React.createRef();

  componentDidMount() {
    console.log("this.picturesRef", this.picturesRef);
    this.setState({ operation: "update" });
    const product = this.props.products.find(
      (item) => item._id === this.props.match.params.id,
    );
    const {
      name,
      price,
      categoryId,
      imgs,
      desc,
      detail,
    } = product;
    this.setState({
      name,
      price,
      categoryId,
      imgs,
      desc,
      detail,
    });
    this.picturesRef.current.setFileList(imgs);
  }

  render() {
    const {
      categories,
      isLoading,
      name,
      price,
      categoryId,
      desc,
      detail,
    } = this.state;
    return (
      <Card title={<div>Add Product</div>} loading={isLoading}>
        <Form
          {...layout}
          onFinish={this.onFinish}
          onFinishFailed={this.onFinishFailed}
          initialValues={{
            name,
            price,
            categoryId,
            desc,
            detail,
          }}
        >
          <Item label="Product Pictures" name="imgs">
            {/**Here I attach picturesRef to this component */}
            <PicturesWall ref={this.picturesRef} />
          </Item>
          <Item {...tailLayout}>
            <Button type="primary" htmlType="submit">
              Submit
            </Button>
          </Item>
        </Form>
      </Card>
    );
  }
}

(P.S. When I use this.picturesRef.current in onFinish(), it works fine.)

Below is the code in PicturesWall

import React, { Component } from "react";
import { Upload, Modal, message } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { BASE_URL } from "../../config";
import { reqPictureDelete } from "../../api";

function getBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = (error) => reject(error);
  });
}

class PicturesWall extends Component {
  state = {
    previewVisible: false,
    previewImage: "",
    previewTitle: "",
    fileList: [],
  };

  handleCancel = () => this.setState({ previewVisible: false });

  handlePreview = async (file) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj);
    }

    this.setState({
      previewImage: file.url || file.preview,
      previewVisible: true,
      previewTitle:
        file.name ||
        file.url.substring(file.url.lastIndexOf("/") + 1),
    });
  };

  handleChange = ({ file, fileList }) => {
    console.log("file=", file);
    const { response, status } = file;

    if (status === "done") {
      if (response.status === 0) {
        fileList[fileList.length - 1].url = response.data.url;
        fileList[fileList.length - 1].name = response.data.name;
      } else {
        message.error(response.msg, 1);
      }
    }
    if (status === "removed") {
      this.deleteImg(file.name);
    }
    this.setState({ fileList });
  };

  deleteImg = async (name) => {
    const response = await reqPictureDelete(name);
    if (response.status === 0) {
      message.success("Successfully Delete", 1);
    } else {
      message.error("Failed", 1);
    }
  };

  getImgNames() {
    let imgs = [];
    this.state.fileList.forEach((item) => {
      imgs.push(item.name);
    });
    return imgs;
  }

  setFileList = (imgNames) => {
    let fileList = [];
    imgNames.forEach((item, index) => {
      fileList.push({
        uid: index,
        name: item,
        url: `${BASE_URL}/upload/${item}`,
      });
    });
    this.setState(fileList);
  };

  render() {
    const {
      previewVisible,
      previewImage,
      fileList,
      previewTitle,
    } = this.state;
    const uploadButton = (
      <div>
        <PlusOutlined />
        <div className="ant-upload-text">Upload</div>
      </div>
    );
    return (
      <div className="clearfix">
        <Upload
          action={`${BASE_URL}/manage/img/upload`}
          method="post"
          listType="picture-card"
          fileList={fileList}
          onPreview={this.handlePreview}
          onChange={this.handleChange}
          name="image"
        >
          {fileList.length >= 4 ? null : uploadButton}
        </Upload>

        <Modal
          visible={previewVisible}
          title={previewTitle}
          footer={null}
          onCancel={this.handleCancel}
        >
          <img
            alt="example"
            style={{ width: "100%" }}
            src={previewImage}
          />
        </Modal>
      </div>
    );
  }
}

export default PicturesWall;

In the first line of componentDidMount, I print out this.picturesRef, and something weird happens: enter image description here in the first line, it shows that current is null, but when I open it, it seems that it has content. However, when I print .current, it is still null.

AKX
  • 152,115
  • 15
  • 115
  • 172
  • How does `PicturesWall` use the ref? – CertainPerformance Aug 24 '20 at 05:06
  • There's a method in class PicturesWall and in that method I setState using the given parameters. – reallyWannaCryIfNOTGetAnswer Aug 24 '20 at 05:09
  • Can you post the PictureWall component code – Asutosh Aug 24 '20 at 05:16
  • I posted it but I don't think it causes the problem, because current is null when `console.log('this.picturesRef',this.picturesRef)` in the first line of componentDidMount – reallyWannaCryIfNOTGetAnswer Aug 24 '20 at 05:24
  • 1
    This [post](https://stackoverflow.com/questions/44074747/componentdidmount-called-before-ref-callback/50019873#50019873) might shed some light. You have props such as `loading` on `Card` which makes me think that perhaps you are initially rendering some "is-loading" type of component on the DOM rather than the children of `Form` such as the `PicturesWall` component. This could be why `PicturesWall` ref is not accessible on the `componentDidMount` lifecycle – 95faf8e76605e973 Aug 24 '20 at 06:16
  • 1
    The "weirdness" occurs because the Chrome inspector prints live objects, not snapshots; by the time you expand the object to inspect it, the `current` prop has been set. – AKX Aug 24 '20 at 06:18
  • Thanks @95faf8e76605e973. Your answer is useful!!! When the is loading, it cannot render the Item with ref! You could answer this question and I will accept your answer. – reallyWannaCryIfNOTGetAnswer Aug 24 '20 at 07:02

2 Answers2

1

As I indicated in the comments section of the OP's question, I noticed that the Card component has a prop loading

<Card title={<div>Add Product</div>} loading={isLoading}>
    <Form>
        <Item>
            <PicturesWall ref={this.picturesRef} />
            ...

This led me to believe that the Card component has conditions which prevented its children from rendering until it is finished loading, an example of this is instead of rendering its children while it's loading - it renders a "is-loading" type of component.

In this scenario, this.picturesRef.current will will return null on the componentDidMount lifecycle because the ref will not be referring to anything because it is not yet in the DOM by that time.


My original comment:

This post might shed some light. You have props such as loading on Card which makes me think that perhaps you are initially rendering some "is-loading" type of component on the DOM rather than the children of Card such as the PicturesWall component. This could be why PicturesWall ref is not accessible on the componentDidMount lifecycle

95faf8e76605e973
  • 13,643
  • 3
  • 24
  • 51
0

This doesn't directly answer your question, but I think you may be Reacting it wrong.

Your componentDidMount function seems to be basically only deriving state from props (and then calling a function on the reffed component). You can derive the state in a class component's constructor, e.g. something like

constructor(props) {
      const product = props.products.find((item)=>item._id === props.match.params.id);
      const {name,price,categoryId,imgs,desc,detail} = product;
      this.state = {name,price,categoryId,imgs,desc,detail};
}

Then, instead of having a setFileList function, you would similarly pass the fileList down to PictureWall as a prop, e.g.

<PicturesWall fileList={this.state.imgs} />
AKX
  • 152,115
  • 15
  • 115
  • 172
  • Thanks, I understand your solution is to pass imgs to the child component and PicturesWall could get it from this.props.fileList. But I'm still confused about this problem. I'm just using the example in React doc [link](https://reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-class-component) – reallyWannaCryIfNOTGetAnswer Aug 24 '20 at 06:16