How to get state from custom hooks to update in "parent" component?

Viewed 92

I am trying to separate some logic from my component into a custom hook. I feel like i'm misunderstanding some fundamentals but I thought my code would work. I basically update my state in my custom useTrip hook, and i want my map component to have that same updated state.

useTrip.js:

export const useTrip = () => {
    const [businesses, setBusinesses] = useState([])
    useEffect(()=>{
        console.log(businesses) //prints expected results
    },[businesses])
    const fetchData = async (name, lat, lng) => {
        const response = await fetch('http://localhost:5000/category/' + lat + "/" + lng + '/' + name)
        const result = await response.json();
        setBusinesses(result)
    }
 return { businesses, fetchData } 
}

Map.js (component that uses useTrip):

export const Map= (props) => {
    const {businesses} = useTrip()
    return(<>
        {businesses.map((.....)}
    </>)
}

Parent.js (parent of map.js):

export const Parent= (props) => {
    const {fetchData} = useTrip()
    useEffect(() => {
        fetchData(title, lat, lng)
    }, [origin])
    return(<>
        
    </>)
}

The businesses is always an empty array when inside the Map component. my code was working before i started refactoring. Isnt the updated state in the custom hook suppose to be consistent across the components that use it?

2 Answers

You must use your custom hook on Parent component, and send the businesses to your Map component via props.

i.e.

function Parent (props) {
  const { fetchData, businesses } = useTrip()

  useEffect(() => {
        fetchData(title, lat, lng)
  }, [origin])

  return (
    <Map businesses={businesses} />
  )
}

function Map (props) {
  const { businesses } = props
  return (
    <>
      {businesses.map(/* ... */)}
    </>
  )
}

If you call your custom hook on each component, they will get their own state

I have played around with this a bit, and come up with a better, solution. It is in the first code block.

import {useEffect, useState} from 'react';
import { v4 as uuidv4 } from 'uuid';

const constant_data = {
    altering_var: null,
    queue: {},
    default_set: false
};

export const useConstantVariable = (defaultUser) => {
    //set an id to a unique value so this component can be identified
    const [id, setId] = useState(uuidv4());
    //use this variable to force updates to screen
    const [updateId, setUpdateId] = useState({});

    //set the data contained in this hook
    const setData = (data) => {
        constant_data.altering_var = data;

    };

    //force an update of screen
    const updateScreen = () => {
        setUpdateId({...updateId});
    };

    //make a copy of the data so it is seen as a new constant instance
    const saveData = () =>{
        //if the value is an array copy the array
        if(Array.isArray(constant_data.altering_var)){
            constant_data.altering_var = [...constant_data.altering_var];

            //if the value is an object copy it with its prototype
        } else if(typeof constant_data.altering_var === 'object' && constant_data.altering_var !== null){

            constant_data.altering_var = completeAssign({}, constant_data.altering_var);
        } else {
            //do no operation on basic types
        }
    }

    //update all instances of this hook application wide
    const updateAll = () => {

        saveData();


        //now get all instances and update them, remove broken links.
            Object.keys(constant_data.queue).map((k)=> {
            const value = constant_data.queue[k];
            if (typeof value !== 'undefined' && value !== null) {
                constant_data.queue[k]();
            } else {
                delete constant_data.queue[k]
            }
            return true;
        });
    };

    //set the function to call to update this component
    constant_data.queue[id] = updateScreen;

    //for the first instance of this hook called set the default value.
    if (typeof defaultUser !== 'undefined'  && !constant_data.default_set) {
        constant_data.default_set = true;
        setData(defaultUser);
    }

    //when this component is destroyed remove all references to it in the queue used for updating.
    useEffect(() => {
        return () => {
            delete constant_data.queue[id];
        };
    }, []);

    //return the new variable to the constant
    return [
        constant_data.altering_var,
        (data) => {
            setData(data);
            updateAll();
        }
    ];
};



function completeAssign(target, source) {
    target = Object.assign(target, source);

    Object.setPrototypeOf(target, Object.getPrototypeOf(source));

    return target;
}

OLD ANSWER

This is how we managed to solve this issue, it is not perfect, and I am open to suggestions for improvements. But we created a user component to share our user across the entire app.

const users = {client: {isSet: () => { return false; }  } }
const instances = {client: []}


export const useClientUser = (defaultUser) => {


    const [updateId, setUpdateId] = useState(uuidv4());
    const setClientUser = (data) => {
        users.client = new Person(data);
    }

    const updateScreen = () => {
        setUpdateId(uuidv4());
    }

    useEffect(()=>{

        if(defaultUser !== '' && typeof defaultUser !== 'undefined'){
            setClientUser(defaultUser);
        }
        instances.client.push(updateScreen);
    }, []);


    return [users.client , (data) => { setClientUser(data);
        instances.client = instances.client.filter((value)=> {
            if(typeof value !== 'undefined'){ return true } else { return false }

        } );
        instances.client.map((value)=> {if(typeof value !== 'undefined') { value() } })


    } ];
}

I have rewritten our component to show how yours would hypothetically work.

import { v4 as uuidv4 } from 'uuid';


//create super globals to share across all components
const global_hooks = {businesses: {isSet: false  } }
const instances = {businesses: []}


export const useTrip = () => {
    //use a unique id to set state change of object
    const [updateId, setUpdateId] = useState(uuidv4());

    //use this function to update the state causing a rerender
    const updateScreen = () => {
        setUpdateId(uuidv4());
    }
    
    //when this component is created add our update function to the update array
    useEffect(()=>{
        instances.businesses.push(updateScreen);
    }, []);

    useEffect(()=>{
        console.log(global_hooks.businesses) //prints expected results
    },[updateId]);

    const fetchData = async (name, lat, lng) => {
        const response = await fetch('http://localhost:5000/category/' + lat + "/" + lng + '/' + name)
        const result = await response.json();
        global_hooks.businesses = result;
        global_hooks.businesses.isSet = true;
    }
    return {businesses: global_hooks.businesses, fetchData: (name, lat, lng) => {
            //fetch your data
            fetchData(name, lat, lng);

            //remove update functions that no longer exist
            instances.businesses = instances.business.filter((value)=> {
                if(typeof value !== 'undefined'){ return true } else { return false }

            } );

            //call update functions that exist
            instances.businesses.map((value)=> {if(typeof value !== 'undefined') { value() } })

        }
    };
}

Related