reactjsreact-image

What is the correct way to dynamically set image src to remote url in React (18)


I'm learning React (v18) and I've tried implementing some similar solutions that I've found here, but so far no success. I have been trying display "cards" with names and pictures from a list of authors; the url for the each picture is remote (for example from wikipedia):

export const Authors = () => {
    const authors = [
        {
            id: 1,
            name: 'Tayeb Salih',
            wikipedia: 'https://en.wikipedia.org/wiki/Tayeb_Salih',
            picture_url: 'https://upload.wikimedia.org/wikipedia/commons/a/ae/Tayeb_saleh.jpg'
        },
        {
            id: 2,
            name: 'Adolfo Bioy Casares',
            wikipedia: 'https://en.wikipedia.org/wiki/Adolfo_Bioy_Casares',
            picture_url: 'https://upload.wikimedia.org/wikipedia/commons/a/a9/Bioy.png'
        },
        {
            id: 3,
            name: 'Lucy Foley',
            wikipedia: 'https://en.wikipedia.org/wiki/Lucy_Foley',
            picture_url: 'https://www.intrinseca.com.br/upload/autor/Lucy%20Foley%20G.jpg'
        },
        {
            id: 4,
            name: 'Matt Ruff',
            wikipedia: 'https://en.wikipedia.org/wiki/Matt_Ruff',
            picture_url: 'https://images.gr-assets.com/authors/1597587995p5/40577.jpg'
        }];  
    const cards:any[] = [];
    authors.forEach((author, index) => {
        const borderColor = index > 1 ? null : index > 0 ? '#57b60a' : '#b60a57';
        const item = (
            <Card borderColor={borderColor} title={author.name} subtitle={author.id.toString()} key={index}>
                <img src={require(`${author.picture_url}`)} alt={author.name} />
            </Card>
        );
        cards.push(item);
    });

    return (
        <section id="section-authors">
            <h1>Authors</h1>
            <div className="group-cards">
                {cards}
            </div>
            <img
                src="https://upload.wikimedia.org/wikipedia/commons/a/a9/Bioy.png"
                alt=""
                height="200"
            />
        </section>
    );
};

In this way I get the following error: Uncaught Error: Cannot find module 'https://upload.wikimedia.org/wikipedia/commons/a/ae/Tayeb_saleh.jpg'.

When I change the code to <img src={require(author.picture_url)} alt={author.name} />, the same error occurs.

If I trying use only <img src={author.picture_url} alt={author.name} />, error: *Uncaught Error: img is a void element tag and must neither have children nor use dangerouslySetInnerHTM*... then, with a little bit of insanity: ```<img src={${author.picture_url}`} alt={author.name} />```... same error...

What's wrong anyway?

EDITED including <Card/>

export const Card = (props: {
  title?: string | null | undefined;
  subtitle?: string | null | undefined;
  borderColor?: string | null | undefined;
  children?: any;
}) => {
    const cardStyle = {
        borderColor: props.borderColor || '#ded3d3',
    };

    return (
        <div className="card-minhoteca" style={cardStyle}>
            <CardHeader title={props.title} subtitle={props.subtitle} />
            <CardBody>
                {
                    React.Children.map(props.children, (child, i) => {
                        return React.cloneElement(child, { ...props, key: i });
                    })
                }
            </CardBody>
            <CardFooter>
                <p>Card Footer</p>
            </CardFooter>
        </div>
    );
};

export const CardHeader = (props: { title?: string; subtitle?: string; }) => {
    return (
        <div className="card-header">
            <h3 className="title">{props.title}</h3>
            <h4 className="card-subtitle">{props.subtitle}</h4>
        </div>
    );
};

export const CardBody = (props: {
    children?: any
}) => {

    return (
        <div className="card-body">
            {props.children}
        </div>
    );
};


export const CardFooter = (props: {
    children?: any;
    callBack?: any;
}) => {
    return (
        <div className="card-footer">
            {props.children}
            <button onClick={(e) => {
                e.preventDefault();
                props.callBack();
            }}>Click!</button>
        </div>
    );
};

Solution

  • UPDATE

    You can use {props.children} in Card to render the children in the place needed. This should solve the error if you also changed the way of using img as the original answer.

    In this use case, the children are not modified in their placement, which would not justify the use of React.cloneElement() over props.children. Hope this will help.

    Example:

    // Replaces Card component
    
    export const Card = (props: {
      title?: string | null | undefined;
      subtitle?: string | null | undefined;
      borderColor?: string | null | undefined;
      children?: any;
    }) => {
      const cardStyle = {
        borderColor: props.borderColor || '#ded3d3',
      };
    
      return (
        <div className="card-minhoteca" style={cardStyle}>
          <CardHeader title={props.title} subtitle={props.subtitle} />
          <CardBody>{props.children}</CardBody>
          <CardFooter>
            <p>Card Footer</p>
          </CardFooter>
        </div>
      );
    };
    

    Original

    Instead of using forEach you can use map() to map out authors.

    This way there is no need to create another cards array so component is cleaner.

    Regarding the img, to pass the url it should be <img src={author.pictureUrl} />, I also changed the property name from picture_url to pictureUrl in authors.

    If the issue remains, the error is in Card component. Maybe {props.children} was not placed properly, but will need to see the Card component to find out.

    Example:

    export const Authors = () => {
      const authors = [
        {
          id: 1,
          name: "Tayeb Salih",
          wikipedia: "https://en.wikipedia.org/wiki/Tayeb_Salih",
          pictureUrl:
            "https://upload.wikimedia.org/wikipedia/commons/a/ae/Tayeb_saleh.jpg",
        },
        {
          id: 2,
          name: "Adolfo Bioy Casares",
          wikipedia: "https://en.wikipedia.org/wiki/Adolfo_Bioy_Casares",
          pictureUrl:
            "https://upload.wikimedia.org/wikipedia/commons/a/a9/Bioy.png",
        },
        {
          id: 3,
          name: "Lucy Foley",
          wikipedia: "https://en.wikipedia.org/wiki/Lucy_Foley",
          pictureUrl:
            "https://www.intrinseca.com.br/upload/autor/Lucy%20Foley%20G.jpg",
        },
        {
          id: 4,
          name: "Matt Ruff",
          wikipedia: "https://en.wikipedia.org/wiki/Matt_Ruff",
          pictureUrl: "https://images.gr-assets.com/authors/1597587995p5/40577.jpg",
        },
      ];
    
      return (
        <section id="section-authors">
          <h1>Authors</h1>
          <div className="group-cards">
            {authors.map((author, index) => {
              const borderColor =
                index > 1 ? null : index > 0 ? "#57b60a" : "#b60a57";
              return (
                <Card
                  borderColor={borderColor}
                  title={author.name}
                  subtitle={author.id.toString()}
                  key={index}
                >
                  <img src={author.pictureUrl} alt={author.name} />
                </Card>
              );
            })}
          </div>
          <img
            src="https://upload.wikimedia.org/wikipedia/commons/a/a9/Bioy.png"
            alt=""
            height="200"
          />
        </section>
      );
    };