react-instantsearch icon indicating copy to clipboard operation
react-instantsearch copied to clipboard

hits DOES NOT UPDATE using the connectHits HOC connector WHEN refresh is set to true on CRUD operations

Open waldothedeveloper opened this issue 4 years ago • 2 comments

Describe the bug 🐛 When using the connectHits connector on react-instantsearch-dom the provided props hits does NOT update after a CRUD operation after refresh has been set to TRUE and back to false as described in the docs.

A clear and concise description of what the bug is. I have the following:

1- create-react-app that uses react-instantsearch-dom with connectHits and connectSearchBox

Here's my AlgoliaInstantSearch Component:

import React from "react";
import algoliasearch from "algoliasearch";
import { InstantSearch } from "react-instantsearch-dom";

const searchClient = algoliasearch(
  process.env.REACT_APP_ALGOLIA_APIKEY,
  process.env.REACT_APP_ALGOLIA_SEARCH_ONLY_KEY
);

// ! Algolia indexes
// weg-alg-prod-index
// weg-alg-dev-index
const index =
  process.env.NODE_ENV === "production"
    ? "weg-alg-prod-index"
    : "weg-alg-dev-index";

const AlgoliaInstantSearch = (props) => {
  React.useEffect(() => {
    if (props.algoliaCache) {
      props.setAlgoliaCache(false);
    }
  }, [props]);
  return (
    <InstantSearch
      indexName={index}
      searchClient={searchClient}
      refresh={props.algoliaCache}
    >
      {props.children}
    </InstantSearch>
  );
};
export default AlgoliaInstantSearch;

2- My data is coming from Firestore Cloud

image

3- Every time a document is created, updated, or deleted it triggers the following Firebase Cloud functions:

const functions = require("firebase-functions");
const algoliasearch = require("algoliasearch");

const client = algoliasearch(
  functions.config().algolia.appid,
  functions.config().algolia.apikey
);

// ! Algolia indexes
// weg-alg-prod-index
// weg-alg-dev-index
const index = client.initIndex("weg-alg-dev-index");

exports.indexClient = functions.firestore
  .document(`ChatRooms/{docId}`)
  .onCreate((snap, context) => {
    const data = snap.data();
    const objectID = snap.data().uniqueId;
    console.log("objectID for a new created client: ", objectID);

    return index
      .saveObject({
        objectID,
        ...data,
      })
      .then(({ objectID }) =>
        console.log("The document was created ok", objectID)
      )
      .catch((e) => console.log(e));
  });

exports.unindexClient = functions.firestore
  .document(`ChatRooms/{docId}`)
  .onDelete((snap, context) => {
    const objectID = snap.data().uniqueId;

    return index
      .deleteObject(objectID)
      .then(() => console.log("The document was deleted ok", objectID))
      .catch((e) => {
        console.log("There was an error with the client delete operation", e);
      });
  });

exports.updateindexClient = functions.firestore
  .document(`ChatRooms/{docId}`)
  .onUpdate((change, context) => {
    const newValue = change.after.data();
    const objectID = change.after.data().uniqueId;

    return index
      .saveObject({
        objectID,
        ...newValue,
      })
      .then(({ objectID }) =>
        console.log("The document was updated ok!", objectID)
      )
      .catch((e) => console.log(e));
  });

Here's an example of the Firebase Cloud functions logs when any of the CRUD functions are triggered:

image

The CRUD operations will set the const [algoliaCache, setAlgoliaCache] = React.useState(false); to TRUE which will get back to FALSE on my AlgoliaInstantSearch component by this hook:

  React.useEffect(() => {
    if (props.algoliaCache) {
      props.setAlgoliaCache(false);
    }
  }, [props]);

Here's my connectHits HOC:

import React from "react";
import { nicePhoneNumber } from "../constants/regex";
import { connectHits } from "react-instantsearch-dom";
import NoSearchResults from "../NoResults/noSearchResults";

const ClientsHits = ({
  hits,
  handleDeleteClientModal,
  handleEditClientModal,
}) => {
  React.useEffect(() => {
    return () => console.log("ClientHits unmounted");
  }, []);

  return hits.length === 0 ? (
    <NoSearchResults />
  ) : (
    <table className="min-w-full">
      <thead>
        <tr>
          <th className="sticky top-0 px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
            Nombre
          </th>
          <th className="sticky top-0 px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
            Compania
          </th>
          <th className="sticky top-0 px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
            Estado
          </th>
          <th className="sticky top-0 px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"></th>
        </tr>
      </thead>
      <tbody className="bg-white">
        {hits.map((hit) => {
          return (
            <tr className="hover:bg-blue-100" key={hit.uniqueId}>
              <td className="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
                <div className="flex items-center">
                  <div className="ml-0">
                    <div className="text-sm leading-5 font-medium text-gray-900 truncate max-w-xs">
                      {`${hit.firstname} ${hit.second_name} ${hit.lastname} ${hit.second_lastname}`}
                    </div>
                    <div className="text-sm leading-5 text-gray-500">
                      {nicePhoneNumber(hit.phone)}
                    </div>
                  </div>
                </div>
              </td>
              <td className="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
                <div
                  className={
                    hit.company === "Ambetter"
                      ? "text-sm leading-5 text-red-800 font-semibold"
                      : hit.company === "Florida Blue"
                      ? "text-sm leading-5 text-blue-500 font-semibold"
                      : hit.company === "Oscar"
                      ? "text-sm leading-5 text-teal-400 font-semibold"
                      : hit.company === "Molina"
                      ? "text-sm leading-5 text-orange-500 font-semibold"
                      : hit.company === "Cigna"
                      ? "text-sm leading-5 text-indigo-500 font-semibold"
                      : "bg-white"
                  }
                >
                  {hit.company}
                </div>
              </td>
              <td className="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
                <span
                  className={
                    hit.active
                      ? "px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
                      : "px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
                  }
                >
                  {hit.active ? "Activo" : "Inactivo"}
                </span>
              </td>
              <td className="px-6 py-4 whitespace-no-wrap text-right border-b border-gray-200 text-sm leading-5 font-medium">
                {/* Edit hit  */}
                <button
                  onClick={() => handleEditClientModal(hit)}
                  className="px-4 text-blue-600 hover:text-blue-900 focus:outline-none focus:underline"
                >
                  <svg
                    className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400 transition duration-300 ease-in-out hover:text-blue-500 transform hover:-translate-y-1 hover:scale-110"
                    fill="currentColor"
                    viewBox="0 0 20 20"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <path
                      d="M17.4142 2.58579C16.6332 1.80474 15.3668 1.80474 14.5858 2.58579L7 10.1716V13H9.82842L17.4142 5.41421C18.1953 4.63316 18.1953 3.36683 17.4142 2.58579Z"
                      fillRule="evenodd"
                    />
                    <path
                      fillRule="evenodd"
                      clipRule="evenodd"
                      d="M2 6C2 4.89543 2.89543 4 4 4H8C8.55228 4 9 4.44772 9 5C9 5.55228 8.55228 6 8 6H4V16H14V12C14 11.4477 14.4477 11 15 11C15.5523 11 16 11.4477 16 12V16C16 17.1046 15.1046 18 14 18H4C2.89543 18 2 17.1046 2 16V6Z"
                    />
                  </svg>
                </button>
                {/* Delete hit  */}
                <button
                  onClick={() => handleDeleteClientModal(hit)}
                  className="px-4 text-blue-600 hover:text-blue-900 focus:outline-none focus:underline"
                >
                  <svg
                    className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400 transition duration-300 ease-in-out hover:text-red-500 transform hover:-translate-y-1 hover:scale-110"
                    fill="currentColor"
                    viewBox="0 0 20 20"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <path
                      fillRule="evenodd"
                      clipRule="evenodd"
                      d="M9 2C8.62123 2 8.27497 2.214 8.10557 2.55279L7.38197 4H4C3.44772 4 3 4.44772 3 5C3 5.55228 3.44772 6 4 6L4 16C4 17.1046 4.89543 18 6 18H14C15.1046 18 16 17.1046 16 16V6C16.5523 6 17 5.55228 17 5C17 4.44772 16.5523 4 16 4H12.618L11.8944 2.55279C11.725 2.214 11.3788 2 11 2H9ZM7 8C7 7.44772 7.44772 7 8 7C8.55228 7 9 7.44772 9 8V14C9 14.5523 8.55228 15 8 15C7.44772 15 7 14.5523 7 14V8ZM12 7C11.4477 7 11 7.44772 11 8V14C11 14.5523 11.4477 15 12 15C12.5523 15 13 14.5523 13 14V8C13 7.44772 12.5523 7 12 7Z"
                    />
                  </svg>
                </button>
              </td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

const HOCClientHits = connectHits(ClientsHits);
export default HOCClientHits;

Here's my connectSearchBox HOC Component:

import React from "react";
import { connectSearchBox } from "react-instantsearch-dom";

const SearchClients = ({
  currentRefinement,
  isSearchStalled,
  refine,
  handleChange,
}) => {
  return (
    <div className="relative py-2">
      <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
        <svg
          className="h-5 w-5 text-gray-500"
          width="20"
          height="20"
          viewBox="0 0 20 20"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            fillRule="evenodd"
            clipRule="evenodd"
            d="M8 4C5.79086 4 4 5.79086 4 8C4 10.2091 5.79086 12 8 12C10.2091 12 12 10.2091 12 8C12 5.79086 10.2091 4 8 4ZM2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8C14 9.29583 13.5892 10.4957 12.8907 11.4765L17.7071 16.2929C18.0976 16.6834 18.0976 17.3166 17.7071 17.7071C17.3166 18.0976 16.6834 18.0976 16.2929 17.7071L11.4765 12.8907C10.4957 13.5892 9.29583 14 8 14C4.68629 14 2 11.3137 2 8Z"
            fill="currentColor"
          />
        </svg>
      </div>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          refine(event.currentTarget.value);
        }}
      >
        <input
          type="search"
          value={currentRefinement}
          onChange={(event) => {
            refine(event.currentTarget.value);
            handleChange(event);
          }}
          className="form-input block w-full pl-10 sm:text-sm sm:leading-5 bg-white border-none focus:shadow-none"
          placeholder="Buscar"
        />
      </form>
    </div>
  );
};

const HOCSearchClients = connectSearchBox(SearchClients);
export default HOCSearchClients;

If hits.length > 0 I will render the HOCClientHits component above, if not, then a similar component is rendered with the data coming from FireStore Cloud.

To Reproduce 🔍

Steps to reproduce the behavior:

  1. Create a Firebase project
  • needs to be on a Blaze plan to make sure the Firebase Cloud functions can call the algoliasearch API.
  1. On the FireStore, create two simple documents that can have this data as an object:
  {
    "fullname": "Great Customer Example",
    "firstname": "Great",
    "second_name": "",
    "lastname": "Customer",
    "second_lastname": "",
    "active": true,
    "color": "teal",
    "company": "Florida Blue",
    "phone": "+16564898765651",
    "secondary_phone": "",
    "third_phone": "",
    "placeholder": "Activo",
    "uniqueId": "dVqYXGy1Y",
    "objectID": "dVqYXGy1Y",
    "unread_messages": []
  },
  1. Render your data coming from FireStore on a map function on your React JS app that conditionally renders hits when searching, or clients otherwise:
   {hits.length > 0 ? (
            <HOCClientHits
              handleDeleteClientModal={handleDeleteClientModal}
              handleEditClientModal={handleEditClientModal}
            />
          ) : (
            <ClientsTable
              clients={clients}
              handleDeleteClientModal={handleDeleteClientModal}
              handleEditClientModal={handleEditClientModal}
            />
          )}
  1. Create, update or delete a document from the FireStore database that will trigger the Firebase Cloud Functions way above this.
exports.unindexClient = functions.firestore
  .document(`ChatRooms/{docId}`)
  .onDelete((snap, context) => {
    const objectID = snap.data().uniqueId;

    return index
      .deleteObject(objectID)
      .then(() => console.log("The document was deleted ok", objectID))
      .catch((e) => {
        console.log("There was an error with the client delete operation", e);
      });
  });
  1. Once it's deleted from the Algolia, make a simple logic that refresh is set to TRUE and back to FALSE as described on the docs.

6: Search for the deleted customer, you will still see it there:

A live example helps a lot! We have a simple online template for you to use for your explanations:

Here's a link to a video where I show the whole process, as you will see on the video, ONLY the second time I hit delete it will refresh the Algolia index.

URL-TO-VIDEO

Open to share my project with the team.

Environment:

image image

Package-JSON file:

{
  "name": "weginternal",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@material-ui/core": "^4.9.5",
    "@tailwindcss/ui": "^0.1.3",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "algoliasearch": "^4.2.0",
    "autoprefixer": "^9.7.4",
    "aws-amplify": "^2.2.6",
    "aws-amplify-react": "^3.1.7",
    "emoji-mart": "^3.0.0",
    "firebase": "^7.9.3",
    "notistack": "^0.9.9",
    "postcss-cli": "^7.1.0",
    "prop-types": "^15.7.2",
    "react": "^16.13.0",
    "react-dom": "^16.13.0",
    "react-instantsearch-dom": "^6.4.0",
    "react-router-dom": "^5.1.2",
    "react-scripts": "3.4.0",
    "react-spring": "^8.0.27",
    "react-transition-group": "^4.3.0",
    "shortid": "^2.2.15",
    "source-map-explorer": "^2.4.2",
    "use-sound": "^1.0.2"
  },
  "scripts": {
    "analyze": "source-map-explorer 'build/static/js/*.js'",
    "build:style": "tailwind build src/styles/index.css -o src/styles/tailwind.css",
    "start": "npm run build:style && react-scripts start",
    "build": "react-scripts build",
    "flow": "flow",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "husky": {
    "hooks": {
      "pre-commit": "pretty-quick --staged"
    }
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "eslint-plugin-prettier": "^3.1.3",
    "husky": "^4.2.5",
    "prettier": "2.0.5",
    "pretty-quick": "^2.0.1",
    "tailwindcss": "^1.2.0"
  }
}

waldothedeveloper avatar May 06 '20 18:05 waldothedeveloper

Thanks for all this information, but as it's quite a lot, I may have missed a piece of information. However, from the title and description, I think I understand the confusion.

The main problem is coming from the fact that refresh isn't continuously being taken in account, but is rather a trigger for a one-time method to clear the cache and search again.

This means that when something updates later, you will still have a cache

The proper solution can be the following ways:

  1. disable cache alltogether (on the search client itself). This is useful in a situation where the data updates frequently

  2. invalidate the cache based on an event from the server:

When you update on firestore, what you can set in place is not only sending events to Algolia when it's updated, but after method.wait() you can send an event (SSE, web socket eg) to the client, which then sets refresh to `true

  1. use that same event to give the user the opportunity to refresh themselves

When refreshing, and the data is different, it's possible that the items users are looking at change, this I can imagine would be annoying to see if you're just inspecting the details of a certain item.

Let me know if those options make sense for you!

Haroenv avatar May 07 '20 08:05 Haroenv

Hi @Haroenv, I know it's a lot of information, but I didn't want to miss anything either, so I when above and beyond to be descriptive in this issue. Because of the characteristics of this web app and the behaviors of the users that will be using it, I'll go with your solution #1 to disable the cache altogether. But I'm curious about how this could best be implemented in my case having that infrastructure, perhaps in the future, you guys can showcase some examples of Algolia integrations with AWS Amplify, Firebase Cloud Functions, etc, for example, NextJS which has a complete folder of examples. That's just a suggestion. On the other hand, option #2 could be another solution, but I really need to deliver this project. The thing here is that it is not easy to put on an example/code sandbox because it would require to set up a Firebase project, put in on a blaze plan, setup GC functions to send the CRUD changes to Algolia, configure the React client, etc...I understand it's a lot. I wasn't aware you can disable the cache. Thanks for taking the time to read my report.

waldothedeveloper avatar May 07 '20 14:05 waldothedeveloper

Hi! We're doing a round of cleanup before migrating this repository to the new InstantSearch monorepo. This issue seems not to have generated much activity lately, so we're going to close it, feel free to reopen if needed.

dhayab avatar Dec 21 '22 16:12 dhayab