...

/

Adding Read-Write Functionality to the ImageListAtom Function

Adding Read-Write Functionality to the ImageListAtom Function

Learn how to implement a like button using Jotai atoms, use read-only, write-only, and read-write atoms, handle asynchronous data fetching, and update UI components based on state changes.

The write functionality in imageListAtom

So far, we have a read-only imageListAtom atom and an async fetchImagesAtom atom. Let’s add a write functionality to imageListAtom atom so that it can accept values from fetchImagesAtom atom:

Press + to interact
// src/atoms/fetchImageAtoms.js
// Define an atom for an image list with an updater function.
export const imageListAtom = atom([], (get, set, newArray) => {
// Update the value of the atom with the new array.
set(imageListAtom, newArray);
});

The atom is ready to receive values, so let’s give it some. We have to go back to the Home component where we kicked off data fetching and add a useEffect, which will update imageListAtom atom. Here’s what the code should look like:

Press + to interact
// src/surfaces/Home.js
// Import the necessary atom hooks.
import { useAtom } from "jotai";
import { fetchImagesAtom, imageListAtom } from "../atoms/fetchImageAtoms";
// Define the Home component.
export const Home = () => {
// Use the fetchImagesAtom to get the JSON data.
const [json] = useAtom(fetchImagesAtom);
// Use the imageListAtom and setAllImages updater function to update the image list.
const [, setAllImages] = useAtom(imageListAtom);
// useEffect to update the image list when json data changes.
useEffect(() => {
if (json) {
// Set the image list to the fetched JSON data.
setAllImages(json);
}
}, [json]);
};

Time to check

This is a good moment to check again whether everything works fine in the app since we just implemented data fetching. If everything is, in fact, working as expected, we’ll move on to implementing functionality for the “Like” button. If we run into any issues, we can start by using console.log to check that the atoms hold and return the values we are expecting them to have.

import React, { useState, useEffect, useReducer } from "react";
import { requestBase } from "./utils/constants";

const ConversationContext = React.createContext();

function ConversationContextProvider({ children }) {
  const [conversationId, setConversationId] = useState(null);

  return (
    <ConversationContext.Provider
      value={{
        conversationId,
        setConversationId,
      }}
    >
      {children}
    </ConversationContext.Provider>
  );
}

function useConversations() {
  const context = React.useContext(ConversationContext);
  if (context === undefined) {
    throw new Error(
      "useConversations must be used within a ConversationContextProvider"
    );
  }
  return context;
}

export { ConversationContextProvider, useConversations };

export const UserStateContext = React.createContext();

export function useUserState() {
  const context = React.useContext(UserStateContext);
  if (context === undefined) {
    throw new Error(
      "useUserState must be used within a UserStateContextProvider"
    );
  }
  return context;
}

const FavoritedContext = React.createContext();

function favoritesReducer(state, action) {
  switch (action.type) {
    case "init_likes": {
      return action.payload;
    }
    case "add_like": {
      const newLikedImage = action.payload;
      return [...state, newLikedImage];
    }
    case "remove_like": {
      const stateWithoutLikedImage = state.filter(
        (item) => item !== action.payload
      );
      return stateWithoutLikedImage;
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function FavoritedContextProvider({ children }) {
  const [loggedInData, setLoggedInData] = useState(null);
  const [state, dispatch] = useReducer(favoritesReducer, loggedInData);

  async function fetchLoggedInData() {
    const response = await fetch(requestBase + "/john_doe/likedImages.json");
    setLoggedInData(await response.json());
  }

  useEffect(() => {
    if (!loggedInData) {
      fetchLoggedInData();
    } else {
      dispatch({ type: "init_likes", payload: loggedInData });
    }
  }, [loggedInData]);

  const value = { state, dispatch };

  return (
    <FavoritedContext.Provider value={value}>
      {children}
    </FavoritedContext.Provider>
  );
}

function useFavorited(userLoggedIn) {
  let context;
  if (userLoggedIn) {
    context = React.useContext(FavoritedContext);
  }

  if (context === undefined) {
    throw new Error(
      "useFavorited must be used within a FavoritedContextProvider"
    );
  }
  return context;
}

export { FavoritedContextProvider, useFavorited };

const BookmarksContext = React.createContext();

function BookmarksContextProvider({ children }) {
  const [bookmarksData, setBookmarksData] = useState(null);
  async function fetchBookmarkData() {
    const response = await fetch(
      requestBase + "/john_doe/bookmarkedImages.json"
    );
    setBookmarksData(await response.json());
  }

  useEffect(() => {
    if (!bookmarksData) {
      fetchBookmarkData();
    }
  }, [bookmarksData]);

  return (
    <BookmarksContext.Provider
      value={{
        bookmarksData,
        setBookmarksData,
      }}
    >
      {children}
    </BookmarksContext.Provider>
  );
}

function useBookmarks() {
  const context = React.useContext(BookmarksContext);
  if (context === undefined) {
    throw new Error(
      "useBookmarks must be used within a BookmarksContextProvider"
    );
  }
  return context;
}

export { BookmarksContextProvider, useBookmarks };
Checking if write functionality is working

Once we are sure that everything is good, we’ll move on to implementing the “Like” button in ImageDetailsModal.

Implementing the “Like” button

The full functionality of the “Like” button in ImageDetailsModal consists of two ...