简体   繁体   中英

React component not getting rendered with map function

My LinkItem component doesn't get rendered when loadAll() is called. It is only rendered after handleClick() or handleChange() is called.

import React, { Component } from "react";
var Datastore = require("nedb");
var db = new Datastore({ filename: __dirname + "/userdata" });
db.loadDatabase(function(err) {

});

function LinkItem(props) {
  console.log("Rendered");
  return (
    <div>
      <a href={props.name}>{props.name}</a>
    </div>
  );
}

export default class List extends Component {
  constructor(props) {
    super(props);
    this.state = {
      link: "",
      data: [],
    };
    this.handleClick = this.handleClick.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.loadAll = this.loadAll.bind(this);
  }

  loadAll() {
    let loadData = [];
    db.find({ type: "link" }, function(err, docs) {
      docs.map(doc => loadData.push(doc.name));
    });
    this.setState({ data: loadData });
  }

  // add new item to data set
  handleClick() {
    this.setState({
      data: [...this.state.data, this.state.link]
    });
    const doc = { name: this.state.link, type: "link" };
    db.insert(doc, (err, docs) => {
      console.log("Inserted");
      console.log(docs);
    });
  }

  // handles form input changes
  handleChange(event) {
    this.setState({ link: event.target.value });
  }

Here in the render function, I am using map to render each LinkItem component. However, they aren't being created at all when I check console.log at my LinkItem component. They only show up once handleClick, and handleChange is called.

  render() {
    const data = this.state.data;

      return (
        <div>
          <button onClick={this.loadAll}>Load data</button>
          <input
            type="text"
            value={this.state.link}
            onChange={this.handleChange}
          />
          <button onClick={this.handleClick}>Add</button>
          {console.log(data)}
          {/* render each data */}
          {data.map((item, index) => (
            <LinkItem name={item} key={index} />
          ))}
        </div>
      );
    }

}

Basically what I want to happen is when loadData is called, all the data is rendered in LinkItem components.

Note: this isn't a duplicate. I checked other questions in stackoverflow where they didn't have a return in the map function. That's not the case for me. It does end up rendered only when other state values change.

EDIT: I was able to fix it by putting setState inside db find and changing the callback to an arrow function. For those of you using nedb with react this might help.

  loadAll() {
    db.find({ type: "link" }, (err, docs) => { // changed to arrow function
      this.setState({ data: docs.map(doc => doc.name)});
    });
  } 

Your db.find function is asynchronous, your program will keep on going and not wait for it to finish :

  loadAll() {
    let loadData = [];
    db.find({ type: "link" }, function(err, docs) {
      docs.map(doc => loadData.push(doc.name));
    });
    this.setState({ data: loadData });
  }

You have 2 solution to fix it.

Putting your setState inside the callback :

  loadAll() {
    let loadData = [];
    db.find({ type: "link" }, function(err, docs) {
      docs.map(doc => loadData.push(doc.name));
      this.setState({ data: loadData });
    });
  }

Or awaiting your call :

  loadAll = async() => {
    let loadData = [];
    const docs = await db.find({ type: "link" });
    docs.map(doc => loadData.push(doc.name));
    this.setState({ data: loadData });
  }

Also, the map function will return a new array with your values. The following syntax is shorter and does the same thing :

loadAll = async () => {
    this.setState({ 
        data: (await db.find({ type: "link" })).map(doc => doc.name)
    });
}

I also noticed you were using your state values inside setState . React recommends using your previous state in a callback to do so and avoid unexpected behavior :

handleClick() {
    this.setState(prevState => {
        data: [...prevState.data, prevState.link]
    });

Another thing you could use is deconstruction, this code :

const data = this.state.data;

Could become :

const { data } = this.state;

You need to call to setState inside your callback, after the documents have been retrieved:

Code

loadAll() {
  let loadData = [];
  db.find({ type: "link" }, function(err, docs) {
    docs.map(doc => loadData.push(doc.name));
    this.setState({ data: loadData });
  });
} 

Or just do:

loadAll() {
  db.find({ type: "link" }, function(err, docs) {
    this.setState({ data: docs.map(doc => doc.name)});
  });
} 

The reason you have to do that is because db.find is an asynchronous function so, it will return inmediatly so loadData will be equal to [] .

You could also declare loadAll as async and use await :

loadAll() {
  const docs = await db.find({ type: "link" });
  this.setState({ data: docs.map(doc => doc.name) });
}

By the way, there are a few tips that will make your code shorter:

  1. Use the object destructuring syntax
  2. Declare your functions as arrow functions , this way you can get rid of the constructor if you create the state directly inside the class

The final code will be something like this:

Final code

import React, { Component } from "react";

var Datastore = require("nedb");
var db = new Datastore({ filename: __dirname + "/userdata" });
db.loadDatabase(function(err) {

});

function LinkItem(props) {
    console.log("Rendered");
    return (
        <div>
            <a href={props.name}>{props.name}</a>
        </div>
    );
}

export default class List extends Component {
    state = {
        link: "",
        data: [],
    }

    loadAll = async () => {
        const docs = await db.find({ type: "link" });
        this.setState({ data: docs.map(doc => doc.name) });
    }

    // add new item to data set
    handleClick = () => {
        const { data, link } = this.state;

        this.setState({
            data: [...data, link]
        });

        const doc = { name: this.state.link, type: "link" };

        db.insert(doc, (err, docs) => {
            console.log("Inserted");
            console.log(docs);
        });
    }

    // handles form input changes
    handleChange = (event) => {
        this.setState({ link: event.target.value });
    }

    render() {
        const { data } = this.state;

        return (
            <div>
                <button onClick={this.loadAll}>Load data</button>
                    <input
                    type="text"
                    value={this.state.link}
                    onChange={this.handleChange}
                    />
                <button onClick={this.handleClick}>Add</button>

                {console.log(data)}
                {/* render each data */}

                {data.map((item, index) => (
                    <LinkItem name={item} key={index} />
                ))}
            </div>
        );
    }
}

Note that render does not need to be declared as an arrow function because it is automatically bound by React .

Let me know if this works, otherwise, tell me what went wrong.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM