Is there a way to display a Solidity array of structs in React?

Viewed 2426

I am trying to display the contents of a Solidity array of structs in a React front-end.

This is my Solidity smart contract. I've created a function which returns the length of the array in question.

pragma solidity ^0.8.0;

contract Project {

    struct Person {
        string name;
        string description;
    }
    
    Person[] public people;

    function getPersonCount() public view returns (uint) {
        return people.length;
    }

}

Here is my front-end React code:

import React from "react";
import Web3 from './web3';
import { ABI } from './ABI';
import { contractAddr } from './Address';

const web3 = new Web3(Web3.givenProvider);
const ContractInstance = new web3.eth.Contract(ABI, contractAddr);

const NewPerson = () => {

    let personCount = 0;

    personCount = ContractInstance.methods.getPersonCount().call();

    return (
        personCount
    );
};

export default NewPerson;

When I run that front-end code, I get this error message:

Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.

Reading answers to questions on here, I thought the problem might be that I need to call my Solidity function asynchronously so that I can return the output of the function rather than a promise. I tried re-writing my React code like so:

import React from "react";
import Web3 from './web3';
import { ABI } from './ABI';
import { contractAddr } from './Address';

const web3 = new Web3(Web3.givenProvider);
const ContractInstance = new web3.eth.Contract(ABI, contractAddr);

const NewPerson = () => {

    let personCount = 0;

    async function handlePerson() {
        personCount = await ContractInstance.methods.getPersonCount().call();
    }

    handlePerson();

    return (
        personCount
    );
};

export default NewPerson;

This did not trigger an error, but instead returned 0 (suggesting that the handlePerson function did not even run).

I then tried another approach:

import React from "react";
import Web3 from './web3';
import { ABI } from './ABI';
import { contractAddr } from './Address';

const web3 = new Web3(Web3.givenProvider);
const ContractInstance = new web3.eth.Contract(ABI, contractAddr);

const NewPerson = () => {


    let personCount = 0;

    async function handlePerson() {
        personCount = await ContractInstance.methods.getPersonCount().call();
        
        return (
            personCount
        );
    }
    return (
        handlePerson()
    );

};

export default NewPerson;

This gave me the same error message I got the first time around:

Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.

I'd be grateful if anyone has any advice to offer, or if anyone has experienced something similar. My goal is to then iterate through the array to display all of its elements, but so far I don't even seem to be able to display the number of elements in it. It's particularly strange since I'm successfully able to call other functions from my smart contract via the React front-end. Many thanks in advance!

Update:

Many thanks to MrFrenzoid for the help. I've re-written my front-end code so that I am now able to query individual elements of the Solidity array:

import React from "react";
import Web3 from './web3';
import { ABI } from './ABI';
import { contractAddr } from './Address';

const web3 = new Web3(Web3.givenProvider);
const ContractInstance = new web3.eth.Contract(ABI, contractAddr);

const NewPerson = () => {
    
    // Using hard-coded personCount for testing purposes
    let personCount = 70;
    let people = [];

    async function handlePeople() {
        for (let i=0; i<personCount; i++) {
            const person = await ContractInstance.methods.people(i).call();
            people.push(person);
        }
        console.log(people);
    }

    handlePeople();

    return (
        null
    );

};

export default NewPerson;

This returns the contents of the array in the console, as expected. The issue I'm still having is querying the length of the Solidity array, so that I can use that to compute personCount (rather than using a hard-coded value as I do above).

import React from "react";
import Web3 from './web3';
import { ABI } from './ABI';
import { contractAddr } from './Address';

const web3 = new Web3(Web3.givenProvider);
const ContractInstance = new web3.eth.Contract(ABI, contractAddr);

const NewPerson = () => {

    let people = [];

    async function handlePersonCount() {
        const personCount = await ContractInstance.methods.getPersonCount().call();
        console.log(personCount);
    }

    async function handlePeople() {
        for (let i=0; i<personCount; i++) {
            const person = await ContractInstance.methods.people(i).call();
            people.push(person);
        }
        console.log(people);
    }

    handlePersonCount();
    handlePeople();

    return (
        null
    );

};

export default NewPerson;

When I run handlePersonCount(), I get this error message: error message

The reason I'm taking this approach at all is because I'm seemingly unable to query the entire Solidity array at once, and instead need to query one element at a time.

Update:

Once I re-deployed the Truffle/Ganache instance in which I was running my local blockchain, I was able to invoke getPersonCount() from the front-end as expected. It seems my error was in not redeploying my local blockchain after creating a new function/variable in the smart contract.

Here is what my final, functioning front-end code looks like:

import React from "react";
import Web3 from './web3';
import { ABI } from './ABI';
import { contractAddr } from './Address';

const web3 = new Web3(Web3.givenProvider);
const ContractInstance = new web3.eth.Contract(ABI, contractAddr);

const NewPerson = () => {

    let people = [];

    async function handlePersonCount() {
        const personCount = await ContractInstance.methods.getPersonCount().call();
        console.log(personCount);
        return(personCount);
    }

    async function handlePeople(qty) {
        for (let i=0; i<qty; i++) {
            const person = await ContractInstance.methods.people(i).call();
            people.push(person);
        }
        console.log(people);
    }

    async function handler() {
        await handlePeople(await handlePersonCount());
    }

    handler();

    return (
        null
    );

};

export default NewPerson;

I added the return line to handlePersonCount() so that I can feed handlePersonCount() into handlePeople() as a parameter, and then use the handler() function to force handlePeople() to wait until hanldePersonCount() has finished before being invoked. There's probably a cleaner way to set up that sequential functionality, but this seems to work.

1 Answers

In response to your last comment, since you already have the array in js, you can do a quick people.length you can also iterate over it using a normal for,

or an

people.foreach((person) => { console.log(person); });

or

for( const person in people){
   console.log(person);
}

If you have any issues let me know, attaching the error and your code below.

Update 1 (How to grab a dynamic array with web3).

Try this: Create a state variable which will hold your peoples array, make it an empty array when defined.

Then, do the following to iterate over each element, and stack them up.

// we create a local variable with the same name as the state variable where were going to hold our people elements.
let peopleArray = [];

// we query our counter.
const peopleCount = ContractInstance.methods.peopleCount().call();

// we iterate over each people and add it to our local variable.
// I start at 1 since in my contracts i first increment the counter
//   and then do else, since the contract calls aren't mutex, 
//   theres a chance for users to be able to trigger a "race condition" by
//   adding two "people" very fast, so fast that since the counter 
//   didn't update the first people was added, the second one will be 
//   processed with the same value as its counter, thats why, try
//   counting first, and then doing anything else, its a good practice! :D.

for (var i = 1; i <= peopleCount ; i++) {
        const person = ContractInstance.methods.people(i).call();
        console.log(person);
        peopleArray.push(person);
  }

// we set the local varible value as our state variable, since it has the same name, you dont need to do {peopleArray: peopleArray}
this.setState({peopleArray});

Your contract should look something like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Project {

struct Person {
    string name;
    string description;
}

Person[] public people;
uint256 public peopleCount;

constructor(){
    peopleCount = 0;
}

function addPerson(string memory name, string memory description) public returns (Person memory) {
    // we do our corresponding checks.
    require(bytes(name).length > 0, "Error: Dont leave the name empty!");
    require(bytes(description).length > 0, "Error: Dont leave the description empty!");

    // We increment the counter first so we avoid a race condition
    peopleCount++;
    
    // Create a person
    Person memory p = Person(name, description);
    
    // Push the person
    people[peopleCount] = p;
    
    // return the created person.
    return p;
}

}

Remember that setting the storage variables (contract global variables such as people and peopleCount) to public, will have solidity create getters for each variable implicitly, so if you have your variables on public, dont bother creating a getter unless you want something to do before returing said data, for example, tracking how many times people accessed to said variables .

Best regards!

Related