Search code examples
javascriptreactjsfirebasereact-context

React 'useContext' hook not re-rendering after context updates with database data


I am using React's Context API to share data that most of my components need.

The Context is initially defined, but shortly receives data from the Firebase database (please see IdeaContext.tsx). I define the context in a functional component and the display component, which returns a small card based on the information received.

However, the component doesn't render when I start the development server with Yarn. Instead, in order to get it to render, I have to write console.log('something') inside the display component and then it suddenly re-renders. However, when I refresh the server, it again doesn't render.

How can I make my component render immediately (or at least after the context updates with the data from the database?)


Code:

Context Definition:

import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import {ideasRef} from './firebase'

function getIdeas() {
  var arr: Array<Idea> = [];

  ideasRef.on('value', (snapshot) => {
      let items = snapshot.val()

      snapshot.forEach( (idea) => {
        const obj = idea.val()
        arr.push({
          title: obj.title,
          description: obj.description,
          keyID: obj.keyID
        })
        console.log(arr)
      })
  })
  return arr
}

const IdeaContextDefaultValues: IdeaContextType = {
  ideas: [],
  setIdeas: () => {},
};

const IdeaContext = createContext<IdeaContextType>(IdeaContextDefaultValues)

const IdeaContextProvider: React.FC = ({ children }) => {
  const [ideas, setIdeas] = useState<Array<Idea>>(
    IdeaContextDefaultValues.ideas);
  
  useEffect( ()=> {
      console.log('getting info')
      setIdeas(getIdeas())
  }, [])


  useEffect( () => {
    console.log('idea change: ', ideas)
  }, [ideas])

  return (
    <IdeaContext.Provider value={{ ideas, setIdeas }}>
      {children}
    </IdeaContext.Provider>
  );
};

Displayer and Card Component

import React, { FC, ReactElement, useContext } from "react";
import IdeaCreator from "./IdeaCreator";
import { IdeaContext } from "./IdeaContext";
import { Idea } from "../t";
import { Link } from "react-router-dom";

const IdeaPost:React.FC<Idea> = ({title, keyID, description}):ReactElement => {

  console.log('Received',title,description,keyID)
  return (
    <div className="max-w-sm rounded overflow-hidden shadow-lg">
      <img
        className="w-full"
        src="#"
        alt="Oopsy daisy"
      />
      <div className="px-6 py-4">
        <div className="font-bold text-xl mb-2"> <Link to={"ideas/" + keyID} key= {keyID}> {title}</Link>  </div>
        <p className="text-gray-700 text-base">{description}</p>
      </div>
    </div>
  );
};

const IdeaDisplay:FC<any> = (props:any):ReactElement => {


  const { ideas, setIdeas } = useContext(IdeaContext)
  console.log('Ideas in display: ', ideas)
  console.log('test') //This is what I comment and uncommend to get it to show

  return (
    <div className="flex flex-wrap ">
      {ideas.map((idea) => {
        console.log(idea)
        console.log('Sending',idea.title,idea.description,idea.keyID)
        console.log(typeof idea.keyID)
        return ( 
          <IdeaPost
            title={idea.title}
            description={idea.description}
            keyID = {idea.keyID}
            key = {idea.keyID * 100}
          />
        );
      })}
    </div>
  );
};

export default IdeaDisplay;

Solution Code:

import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import {ideasRef} from './firebase'

async function getIdeas() {
  var arr: Array<Idea> = [];

  const snapshot = await ideasRef.once("value");

  snapshot.forEach((idea) => {
    const obj = idea.val();
    arr.push({
      title: obj.title,
      description: obj.description,
      keyID: obj.keyID,
    });
    console.log(arr);
  });

  return arr
}

const IdeaContextDefaultValues: IdeaContextType = {
  ideas: [],
  setIdeas: () => {},
};

const IdeaContext = createContext<IdeaContextType>(IdeaContextDefaultValues)

const IdeaContextProvider: React.FC = ({ children }) => {
  const [ideas, setIdeas] = useState<Array<Idea>>(
    IdeaContextDefaultValues.ideas);
  
    useEffect(() => {
      console.log("getting info");
      const setup = async () => {
        const ideas = await getIdeas();
        setIdeas(ideas);


      };

      setup()


    }, []);
  


  useEffect( () => {
    console.log('idea change: ', ideas)

    const updateDatabase = async () => {
        await ideasRef.update(ideas)
        console.log('updated database')
    }

    updateDatabase()

  }, [ideas])

  return (
    <IdeaContext.Provider value={{ ideas, setIdeas }}>
      {children}
    </IdeaContext.Provider>
  );
};

export {IdeaContext, IdeaContextProvider} 

Solution

  • First of all you would need to use once and not on if you want to get the data only once. If you want to use a realtime listener you could send the setIdeas to your function. Also try to be carefull with async/away calls to the Firebase sdk. Your code could look like this:

    import React, { createContext, useEffect, useState } from "react";
    import { IdeaContextType, Idea } from "../t";
    import { ideasRef } from "./firebase";
    
    async function getIdeas() {
      var arr: Array<Idea> = [];
    
      const snapshot = await ideasRef.once("value");
    
      let items = snapshot.val();
    
      snapshot.forEach((idea) => {
        const obj = idea.val();
        arr.push({
          title: obj.title,
          description: obj.description,
          keyID: obj.keyID,
        });
        console.log(arr);
      });
    
      return arr;
    }
    
    const IdeaContextDefaultValues: IdeaContextType = {
      ideas: [],
      setIdeas: () => {},
    };
    
    const IdeaContext = createContext < IdeaContextType > IdeaContextDefaultValues;
    
    const IdeaContextProvider: React.FC = ({ children }) => {
      const [ideas, setIdeas] =
        useState < Array < Idea >> IdeaContextDefaultValues.ideas;
    
      useEffect(() => {
        console.log("getting info");
        const getData = async () => {
          const ideas = await getIdeas();
          setIdeas(ideas);
        };
      }, []);
    
      useEffect(() => {
        console.log("idea change: ", ideas);
      }, [ideas]);
    
      return (
        <IdeaContext.Provider value={{ ideas, setIdeas }}>
          {children}
        </IdeaContext.Provider>
      );
    };