DEV Community

Richard Haines
Richard Haines

Posted on • Updated on

use-cloudinary hooks lib - useSearch

This article was originally posted on my blog at garden.richardhaines.dev

Following on from my article about the use-cloudinary useUpload hook - use-cloudinary - useUpload hook, this time we'll be taking a look at the useSearch hook, and its a powerful beast!

My use case is simple, Users upload their profile and cover image to cloudinary via the useUpload hook in devpack. Each image is saved to a corresponding folder with their name and a suffix of either cover or profile. When the user next logs in to devpack they can choose to upload one of their previous 3 images that were saved in their cloudinary folders, or just upload a new image and use that one.

This is where useSearch comes into play. I use it by calling a serverless function which I pass a search function with certain search parameters. Lets look at some code to get an idea of what Im talking about.

Serverless function

const cloudinary = require("cloudinary").v2;
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET
});
exports.handler = async event => {
  const body = JSON.parse(event.body);
  const res = await cloudinary.search
    .expression(body.expression.expression)
    .max_results(3)
    .execute()
    .then(result => result);
  return {
    statusCode: 200,
    body: JSON.stringify(res)
  };
};
Enter fullscreen mode Exit fullscreen mode

This function takes an event:

  • Parses the body passing in the expression which defines our search parameters which we will define in our search function below. (Note that at the time of writing this is nested)
  • Sets the max results to 3
  • Returns an a okey 200 and the result of the search

useSearch

I've removed styles from this component

import React from "react";
import Button from "./button";
import LazyImage from "../hub/dev-card/image";
import Error from "../svg/error";
import Loading from "../svg/loading";
import { useSearch } from "use-cloudinary";
import { DevCardStateContext } from "../../context/devcard-context";

const SavedProfileImages = () => {
  const { search, data, isLoading, isSuccess, isError, error } = useSearch({ endpoint: "/.netlify/functions/search" });

  // This is just a standard context with some values, such as name
  const state = React.useContext(DevCardStateContext);

  // The folder name cant have any spaces so we replace the spaces
  let folder = `${state.name.replace(/\s/g, "")}-profile`;

  const customConfigSearch = React.useCallback(
    () => search({ expression: `resource_type:image AND folder=${folder}`, config: {} }),
    [search, folder]
  );

  if (isLoading) return <Loading />;
  if (isError)
    return (
      <div>
        <Error width="90px" height="90px" />
        <p>{error.message}</p>
      </div>
    );

  if (state.name.replace(/\s/g, "") !== "" && isSuccess) {
    return (
      <div>
        <div>
          {data &&
            data.resources
              .filter(image => image.folder === getFolder())
              .slice(0, 3)
              .map(image => {
                return (
                  <LazyImage
                    key={image.etag}
                    publicId={image.public_id}
                    width={100}
                    height={100}
                    transforms={{ height: 0.2, border: "2px_solid_black" }}
                    cloudName="xxxxxxx"
                    secure_url={image.secure_url}
                  />
                );
              })}
        </div>
        <em>Your last 3 saves are shown</em>
      </div>
    );
  }

  return (
    <div>
      <Button text="Load Profile Images" onClick={() => customConfigSearch()} />
      <em>Your last 3 saves are shown</em>
    </div>
  );
};

export default SavedProfileImages;
Enter fullscreen mode Exit fullscreen mode

Lets break this down.

  • The useSearch hook takes an endpoint, in our case our serverless function. It returns a search function, the data and some states.
  • The context holds our users name.
  • The customConfigSearch is the power house of this functionality. You can pass it any number of things to search by, in our case its the folder name. We use the = symbol to indicate that we want an exact match. Its wrapped in a useCallback to prevent re-rendering based on the prop changes and stop our serverless function being called again and again. This saves us on our rate limit with cloudinary.
  • The expression itself is written using the string literal syntax stating the type of media we are searching for and the exact match on the folder name.
  • The config object is optional and can also include any number of options such as tags, sort_by, max_results and many more. Note that some of these can also be added at the serverless function level. Check the docs for more info on whats possible
  • The states are handled with custom error and loading components- If the folder is a match and the data was retrieved successfully then the data array is accessed
  • The image is returned as a lazy image, which we will cover next!
  • A button calls the customConfigSearch function to load the images.

Phew! That was a lot to take in! 😅

The lazy image is a component that lazy loads an image with a placeholder. Handy!

Lazy load image

import React from "react";
import { useImage } from "use-cloudinary";
import { DevCardDispatchContext } from "../../../context/devcard-context";

// Image w/ Lazy-load + placeholder
function LazyImage({ publicId, transformations, width, height, cloudName, secure_url, isProfile }) {
  const { generateUrl, blurredPlaceholderUrl, isError, error, ref, supportsLazyLoading, inView, isSuccess } = useImage({
    cloudName
  });
  const dispatch = React.useContext(DevCardDispatchContext);
  React.useEffect(() => {
    generateUrl({
      publicId,
      transformations: {
        width,
        height,
        ...transformations
      }
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  if (isError) return <p>{error.message}</p>;
  return (
    <div
      ref={!supportsLazyLoading ? ref : undefined}
      style={{
        width: `${width}px`,
        height: `${height}px`,
        background: `no-repeat url(${blurredPlaceholderUrl(publicId, width, height)})`,
        cursor: "pointer",
        outline: "none"
      }}
      role="button"
      tabindex="0"
      onKeyPress={() =>
        dispatch({ type: isProfile ? "selectedProfileImage" : "selectedCoverImage", payload: secure_url })
      }
      onClick={() => dispatch({ type: isProfile ? "selectedProfileImage" : "selectedCoverImage", payload: secure_url })}
    >
      {inView || (supportsLazyLoading && isSuccess) ? (
        <img
          src={secure_url}
          loading="lazy"
          style={{
            width: `${width}px`,
            height: `${height}px`,
            border: "solid 2px"
          }}
          alt="Lazy loaded profile"
        />
      ) : null}
    </div>
  );
}

export default LazyImage;
Enter fullscreen mode Exit fullscreen mode

One thing to note about using this component with Gatsby is that the base image url wont load the image, you have to use the https secure_url for it to work.

And thats it! It's a pretty powerful way to load your images via user defined events. Check the useSearch docs for a run down of whats possible with the hook.

Top comments (0)