re-base
re-base copied to clipboard
Re-base with react hooks
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?
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
@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);
}
}, [])
}
@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);
}
}, [])
@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
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!
@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.
@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.
I ended up using firebase directly within react, it's actually quite easy to use.
@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());
});
}, []);
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
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];
}
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.)
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])
...
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]);
...
}
@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!
@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?
@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
@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!