Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-base with react hooks #264

Open
ozgunbal opened this issue Nov 4, 2018 · 18 comments
Open

Re-base with react hooks #264

ozgunbal opened this issue Nov 4, 2018 · 18 comments

Comments

@ozgunbal
Copy link

ozgunbal commented Nov 4, 2018

Is there any way to accomplish syncState with react hooks (https://reactjs.org/docs/hooks-intro.html). Because there's no this context in functional component, i cannot manage to use re-base.

In theory, I should be sync by using useEffect hook.

Any ideas?

@ozgunbal
Copy link
Author

ozgunbal commented Nov 4, 2018

I came up with a solution already and looks like it works as expected:

import React, { useState, useEffect } from 'react';
import Fish from './Fish';
import base from '../base';

const App = ({ match: { params: { storeId } } }) => {
  const [fishes, setFishes] = useState({});

  useEffect(() => {
    const ref = base.syncState(`${storeId}/fishes`, {
      context: {
        setState: ({ fishes }) => setFishes({ ...fishes }),
        state: { fishes },
      },
      state: 'fishes'
    })

    return () => {
      base.removeBinding(ref);
    }
  }, [])

  return (
    <div>
        <ul className="fishes">
          {Object.entries(fishes).map(([key, fish]) => <Fish key={key} details={fish} />)}
        </ul>
      </div>
    </div>
  )
}

What I did is imitating the context this, with dummy setState and state objects

@ozgunbal
Copy link
Author

ozgunbal commented Nov 5, 2018

@qwales1 If you need to implement custom hooks for your package (of course when react hooks are stable), please inform me.

I can came up with something similar to below:

/**
 * Use to sync your component state with firebase database
 * @param {String} endpoint 
 * @param {Object} options 
 */
export const useSyncState = (endpoint, {
  state,
  setState
}) => {
  useEffect(() => {
    const stateName = Object.keys(state)[0];
    const ref = base.syncState(endpoint, {
      context: {
        setState: (stateChange) => setState({ ...stateChange[stateName] }),
        state,
      },
      state: stateName
    })

    return () => {
      base.removeBinding(ref);
    }
  }, [])
}

@qwales1
Copy link
Collaborator

qwales1 commented Nov 5, 2018

@ozgunbal that is awesome! Would love to include custom hooks. reading the link you sent the thing that jumped out at me as possibly being an issue for syncState or syncDoc was how to get re-base to save the data back to database when you update the component state. for instance, if you added an input in the component and called setFishes to add a new fish, does that actually save the data with your custom hook? I was thinking maybe it would need a function that wraps setFishes something like below but was unsure.

const [fishes, setFishes] = useState({});

//define a function here and use that in the component instead of setFishes directly
const mySetFishes = ({ fishes }) => setFishes({ ...fishes });

useEffect(() => {
  const ref = base.syncState(`${storeId}/fishes`, {
    context: {
      setState: mySetFishes, //<-- pass it in here
      state: { fishes },
    },
    state: 'fishes'
  })

  return () => {
    base.removeBinding(ref);
  }
}, [])

@ozgunbal
Copy link
Author

ozgunbal commented Nov 5, 2018

@qwales1 you're are right, I met the issue of not updating the firebase after the change of component's state. When I dig into the yours source code, I found rebase overwrites the setState function of the component. However react hooks against such mutation. My current hack is calling rebase.post manually after the state change.
Maybe wrapping both of listen and post manually works for imitating sync.

Thanks for your interest. If I found reusable solution for hooks, will definitely send a PR

@LiteSoul
Copy link

Looks like we are doing the same course from Wes Bos but with the new paradigm from React... anyways your workaround didn't work in my case, database doesn't get updated/synced.
Most React libraries are currently being updated to this new Hooks paradigm to simplify codebase and usage, I would argue this library needs to do it as well, cheers!

@ozgunbal
Copy link
Author

@LiteSoul if you want to check, my whole implementation is here: https://github.com/ozgunbal/my-online-courses/tree/master/beginner-react-with-hooks

I used base.sync for taking updates from firebase but send manually all state change with base.post. Given fake context would not change, due to with next render new object reference is made for state. At least, my investigation leads me this far, if I'm right.

@saqibameen
Copy link

saqibameen commented Feb 15, 2019

@ozgunbal @qwales1 I'm having the same issue. I'm trying to implement the states with custom hooks. below is my code.

The fishes hook.

function useFishesHook(init) {
    const [fishes, setFishes] = useState(init);

    function addFish(fish) {
        // Obtain a copy of current fishes.
        const currentFishes = { ...fishes }; // Don't do currentFishes = fishes => Deep copy.
        // Add new fish.
        currentFishes[`fish-${Date.now()}`] = fish;
        // Update the fishes.
        setFishes(currentFishes);
    }

    function loadSampleFishes() {
        setFishes(sampleFishes);
    }

    return {
        fishes,
        addFish,
        setFishes,
        loadSampleFishes
    }
}

Implementing it in the functional component.

export default function App(props) {
    // The custom state hooks.
    const fishHook = useFishesHook({});
    const orderHook = useOrderHook({});

    // UseEffect.
    useEffect(() => {
        const ref = base.syncState(`${props.match.params.storeId}/fishes`, {
            context: {
                setState: ({ fishes }) => fishHook.setFishes({ ...fishes }),
                state: { fishes: fishHook.fishes },
            },
            state: 'fishes'
        });
        return () => {
            base.removeBinding(ref);
        }
    }, [])

    return (
        <div className="catch-of-the-day">
            <div className="menu">
                <Header tagline="Fresh Seafood Market" />
                <ul className="fishes">
                    {
                        Object.keys(fishHook.fishes).map((fishName) => {
                            return (
                                <Fish
                                    fish={fishHook.fishes[fishName]}
                                    addToOrder={orderHook.addToOrder}
                                    key={fishName}
                                    id={fishName}
                                />
                            )
                        })
                    }
                </ul>
            </div>
            <Order fishes={fishHook.fishes} order={orderHook.order} />
            <Inventory addFish={fishHook.addFish} loadSampleFishes={fishHook.loadSampleFishes} />
        </div>
    )
}

I'm able to achieve same functionality using custom hooks but re-base is not working for me. I tried playing around with the code. I put some fishes manually in firebase, they are loaded automatically when I open the store. But once I click on Load Samples which populate the fishes the are updated on the screen but not synced with the firebase.

@LiteSoul
Copy link

I ended up using firebase directly within react, it's actually quite easy to use.

@saqibameen
Copy link

@LiteSoul I ended up using firebase direct as well. Here's how I did it with custom hooks.

// Lifecycle hook for firebase.
    useEffect(() => {
        // Grab reference to the store.
        let ref = firebase.db.ref(`${params.storeId}/fishes`);
        // Sync the data.
        ref.on('value', snapshot => {
            if (snapshot.val())
                fishHook.setFishes(snapshot.val());
        });
    }, []);

@mattwendzina
Copy link

Hi there,

I have also been trying to implement this using useEffect, but with no success.

I think I have it all set up correctly but I'm getting no response whatsoever from Firebase when I'm loading my items into state. Would anyone mind having a quick look at my code to see if they can spot the issue?
This is what my App component looks like below, and here is a link to my GitHub Repo:
https://github.com/mattwendzina/react-ordering-app

const App = props => {
  const [menuItems, setMenuItems] = useState({});
  const [cart, setCart] = useState({});

  const { params } = props.match;

  useEffect(() => {
    const ref = base.syncState(`${params.id}/menuItems`, {
      context: {
        setState: ({ menuItems }) => setMenuItems({ ...menuItems }),
        state: { menuItems }
      },
      state: "menuItems"
    });
    
    return () => {
      base.removeBinding(ref);
    };
  }, []);

  const addItem = item => {
    const newItem = menuItems;
    newItem[`item${Date.now()}`] = item;
    setMenuItems({ ...menuItems, ...newItem });
  };

  const loadSampleItems = () => {
    setMenuItems({ ...menuItems, ...sampleItems });
  };

  const addToCart = key => {
    const order = cart;
    order[key] = order[key] + 1 || 1;
    setCart({ ...order });
  };

  return (
    <div className="App">
      <Header
        title="Better Burgers"
        city={`${props.match.params.id}`}
        caption="We get em right, first time, every time!"
      />
      <div className="componentsContainer">
        <Menu
          menuItems={menuItems}
          addToCart={addToCart}
          formatter={formatter}
        />
        <Order cart={cart} menuItems={menuItems} />
        <Inventory
          addItem={addItem}
          loadSampleItems={loadSampleItems}
          menuItems={menuItems}
          setMenuItems={setMenuItems}
        />
      </div>
    </div>
  );
};

Many Thanks

@akatopo
Copy link

akatopo commented Jul 7, 2019

Think I managed to make this work. Using firebase directly (as proposed above) is probably a better idea.

import { useState, useEffect, useRef } from 'react';

const useEffectCallerSym = Symbol('useEffect caller');

export default useSyncState;

function useSyncState(base, endpoint, path, initialValue) {
  const [stateParam, setStateParam] = useState(initialValue);
  const [cbState, setCbState] = useState({ cb: () => {} });
  // dummy context object to make re-base happy
  const contextRef = useRef({
    setState: (updater, cb) => {
      // if the call did not originate from our useEffect,
      // set the state param as if the state object came
      // from React.Component.setState
      if (updater && updater.sym !== useEffectCallerSym) {
        setStateParam({ ...updater[path] });
      }

      // callback provided by re-base
      if (cb) {
        // call re-base's callback after rendering
        setCbState({ cb });
      }
    },
    state: { [path]: undefined },
  });

  useEffect(() => {
    const firebaseRef = base.syncState(endpoint, {
      context: contextRef.current,
      state: path,
    });

    return () => base.removeBinding(firebaseRef);
  }, [base, endpoint, path]);

  useEffect(() => {
    const context = contextRef.current;
    context.state[path] = stateParam;
    context.setState({
      sym: useEffectCallerSym,
      [path]: stateParam,
    });
  }, [stateParam, path]);

  useEffect(() => {
    cbState.cb();
  }, [cbState]);

  return [stateParam, setStateParam];
}

@adelespinasse
Copy link

Seems like react-firebase-hooks is probably the thing to use for Firebase in functional components. (I'm just starting to try it out, so I can't vouch for its quality, but it seems well designed.)

@aedwards87
Copy link

aedwards87 commented Sep 9, 2020

Similar to above, I used firebase directly but it wasn't quite working for me, it was working if data was already in firebase but it wasn't saving it. I had to use the update function to update firebase with any new data.

  const App = ({ match: { params: { storeId } } }) => {
  const [fishes, setFishes] = useState({})

  useEffect(() => {
      firebase.database().ref(`${storeId}/fishes`).on('value', snapshot => {
          if (snapshot.val()) setFishes(snapshot.val())
      })
  }, []);

  useEffect(() => {
     firebase.database().ref(`${storeId}/fishes`).update(fishes)
  }, [fishes])

  ...

@Eric-Alain
Copy link

The solution I got working was based on @aedwards87 's answer above (thanks so much, you're a life saver).

I had to modify a few things to suit my needs, given how I decided to structure my state. I take no credit for the implementation, simply sharing in the hopes that it helps someone else.

//In firebase config file, in my case, base.js
import firebase from 'firebase/app';
import 'firebase/database';

const firebaseApp = firebase.initializeApp({
  //Insert your own credentials here
  apiKey: 'XXXXXX',
  authDomain: 'XXXXXX',
  databaseURL: 'XXXXXX',
  projectId: 'XXXXXX'
});

export default firebaseApp;


//App.js
import React, { useState, useEffect } from 'react';
import firebaseApp from '../base';

const App = ({ match }) => { 
  const [state, setState] = useState({
    fishes: {},
    order: {}
  });

  useEffect(() => {
    firebaseApp
      .database()
      .ref(`${match.params.storeId}/fishes`)
      .on('value', (snapshot) => {
        if (snapshot.val())
          setState((prev) => {
            return {
              ...prev,
              fishes: snapshot.val()
            };
          });
      });
  }, []);

  useEffect(() => {
    firebaseApp.database().ref(`${match.params.storeId}/fishes`).update(state.fishes);
  }, [state.fishes]);

...
}

@Memnoc
Copy link

Memnoc commented Nov 29, 2022

@Eric-Alain thanks for rationalising the solution here, life saver!
I ended up creating the same exact implementation as our implementation was very similar.

Thank you!

@Memnoc
Copy link

Memnoc commented Nov 29, 2022

@Eric-Alain one thing I am struggling to understand is: did you get something like base.removeBinding() to work with this code?
How do you clean up the DB entries when you switch store?

@Eric-Alain
Copy link

@Memnoc Ouf... It's been a while since I played around with Firebase, so you'll have to forgive me for not really knowing how to respond to your question.

However, it looks like my base.js file is actually different than what was proposed above. Can't remember why, but I guess I didn't deem the other details relevant at the time of adding to this forum. You can see the file here: https://github.com/Eric-Alain/catch-of-the-day-ea/blob/master/src/base.js

My implementation above was part of a learning project I completed.

You're more than welcome to look around the repo and see if there's anything helpful: https://github.com/Eric-Alain/catch-of-the-day-ea

@Memnoc
Copy link

Memnoc commented Nov 30, 2022

@Eric-Alain thank you so much! This is gonna be very helpful. It looks like you have changed the implementation due to the local storage use, but at a glance, it seems you do not do that steps Wes talks about, when he unbinds the state from firebase. I am not even sure it's necessary at this point - I'll leave it as it for now, and move on, but thank you so much for the repo, it's gonna come in handy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants