React component state persist from firebase - reactjs

I have a simple shopping site, with the data coming from a headless CMS system. I am using nextJS/React to build out the site. When a user adds items to the cart, I want to use firebase to persist the state, as the user navigates different pages. However, I lose all data in the state as well as firebase, when the user navigates to different pages. The main problem is I can't set the initial cart state to the firebase data. How do I solve this problem, so i can use the firebase data to persist cart state?
import React, { useState, useEffect } from 'react';
import Layout from './../components/Layout';
import contextCart from './../components/contextCart';
import firebase from '../data/firestore';
export default function MyApp({ Component, pageProps }) {
//any way to set the initial cart state to data from firebase???
const [ cart, setCart ] = useState([]);
const addToCart = (product) => {
setCart((prevCartState) => {
return [ ...prevCartState, product ];
});
firebase.database().ref().set(cart);
};
//componentDidUpdate => listen for cart changes and update the localStorage
useEffect(
() => {
firebase.database().ref().set(cart);
},
[ cart ]
);
//componentDidMount => run the effect only once and run cleanup code on unmount
useEffect(() => {
//effect code
firebase.database().ref().once('value').then(function(snapshot) {
setCart(snapshot.val() || []);
});
//returned function will be called on component unmount aka cleanup code
//return () => { /*any cleanup code*/ };
}, []);
return (
<contextCart.Provider value={{ cart: cart, addToCart: addToCart }}>
<Layout>
<Component {...pageProps} />
</Layout>
</contextCart.Provider>
);
}

The data is not updating because you are not listening to the changes in your code.
You specify to get the data once in the useEffect Hook , which only runs on the initial load.
To listen to changes you can implement it like this:
var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', function(snapshot) {
updateStarCount(postElement, snapshot.val());
});
read more about it in the official firebase docs here: https://firebase.google.com/docs/database/web/read-and-write
I would recommend you to use a state-management library like Redux or even the Context API from React itself.
The reason for this is that firebase charge you for the traffic to your database (this is a bigger issue for the firestore db than the real-time db but still there are costs to consider).
With a state library, you can check if you need to fetch the data from firebase or if you already have what you need in your state.
Also if you can split your code up with the Provider Pattern, this makes it easier to maintain the logic while your application is growing.

Related

How to delay rendering in React Native or make rendering happen after the data is found

Image of Code
So I had a question about how to run rendering in react native after data is loaded from a database. In my code, I want to list prescriptions, but it keeps giving errors as it tries to load the prescription in the rendering function before executing the code that reaches out to firebase and gets the prescription data. How do I make it so that the rendering happens after the firebase data is gathered.
The easiest way to do this is to just have a loading state in React, this can default to true and once the data has been retrieved from Firebase you set to false. Then in your jsx you can return a loader or similar while the loading state is true and only render the rest of the screen that relies on the Firebase data once it's available and loading is set to false. Here's a minimal demo of this concept:
https://codesandbox.io/s/great-shockley-i6khsm?file=/src/App.js
import { ActivityIndicator, Text } from "react-native";
const App = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
// This is where you would have your Firebase function.
setTimeout(() => setLoading(false), 5000);
}, []);
if (loading) {
return <ActivityIndicator />;
}
return <Text>This is my app showing Firebase data</Text>;
};
export default App;
If you want to read a bit further and handle a potential error state if the Firebase function fails then here's a neat article to avoid an anti-pattern having a loading, success and error state. https://dev.to/tehaisperlis/the-loading-anti-pattern-7jj
If you look at the documentation here (https://rnfirebase.io/firestore/usage)
this is there get() example
import firestore from '#react-native-firebase/firestore';
const users = await firestore().collection('Users').get();
const user = await firestore().collection('Users').doc('ABC').get();
This means that you have to get() this data through async/await so do this below
useEffect(()=>{
const fetch = async ()=>{
/*
Your get code here
*/
const users = await firestore().collection('Users').get();
}
// Call fetch
fetch();
},[])

Getting React to only load a component after an action has dispatched and completed

I have a component 'DashboardServiceEntry' that is used as an entry point to Dashboard.
'Dashboard' reads from Redux the current Options.viewLevel and uses this to make an API call and to show what to display.
Dispatching the action in useEffect in 'DashboardServiceEntry', updates the redux Store, but before this is available in the store The 'Dashboard' component is rendered and reads the wrong value in the redux store, triggering an unnecessary api call.
When the redux Store is updated correctly, another API call (the correct one is started).
How can I make it that the Dashboard component is loaded only after the store has been updated?
Thank you kind people.
import Dashboard from './Dashboard';
import { updateDashOptions } from '../../actions';
import { useDispatch } from 'react-redux';
export default function DashboardServiceEntry() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(
updateDashOptions({
viewLevel: 1,
viewLevel1Title: `Service List`,
})
);
}, [dispatch]);
return <Dashboard></Dashboard>;
}
function Dashboard() {
let options = useSelector((state) => state.options);
useEffect(() => {
const loadData = () => {
if (options.viewLevel === 0) {
let uri = createURI(options, options.view);
dispatch(fetchData(uri, 'SERVICES_SL', dispatch));
}
if (options.viewLevel === 1) {
let uri = createURI(options, 'service');
dispatch(fetchData(uri, 'SERVICES_SLSSD', dispatch));
}
};
loadData();
}, [dispatch, options]);
To avoid unnecessary api call, you can dispatch another action like isLoading:true and update it to false when you update the correct value of the options and only when its false, render the Dashboard component.

How to properly refetch data when reloading in any page?

So I am fetching data from my backend on a landing page in my app. This data is used in different parts, so I use redux for global state. The code is something like this:
//Landing Component
export default function Landing() {
const data = useSelector(state => state.data);
useEffect(() => {
if (!data.isAvailable) {
fetchData();
}
...
}, [])
...
}
//Some Component
export default function SomeComponent() {
const data = useSelector(state => state.data)
...
}
//Another Component
export default function AnotherComponent() {
const data = useSelector(state => state.data)
...
}
However as we know, if the user refreshes the page, the data stored in redux is re-initialized. The problem is, if the user refreshes while on the other pages, data does not get refetched. So what I do is I repeat the same useEffect in every other component that needs the data in order to refetch it in the event the user reloads in a different page.
//Some Component
export default function SomeComponent() {
const data = useSelector(state => state.data)
useEffect(() => {
if (!data.isAvailable) {
fetchData();
}
...
}, [])
...
}
I'm sure there are better ways to do this. I've heard of persisting data through localStorage or using redux-persist. But this data can become quite large so I don't think localStorage will do. Something like a global useEffect that will rerun whatever page the user may be. Thank you!
There is one problem with your approach of calling fetchData in useEffect. If you extend it to multiple components you would end up calling fetchData multiple times.
There isn't a silver bullet solution for this problem. However, one reusable solution would be to dispatch an initializer action and listen in the required reducer using an async thunk and hydrate its own store properties.

React context not saving updates

I created a react project for music/artists.
I'm using the React context for accessing artists data across components:
import React, { createContext, useState } from "react";
export const ArtistContext = createContext(null);
const ArtistContextProvider = props => {
const [artists, setArtists] = useState([]);
const addArtist = new_artist => {
setArtists([...artists, new_artist])
};
return (
<ArtistContext.Provider value={{artists, addArtist}}>
{props.children}
</ArtistContext.Provider>
);
};
export default ArtistContextProvider;
I can successfully get the data from the artist context object and also execute de addArtists methods, but the artist I pass to the function is not saved and the variable stills returns the default value.
My usage in a component:
const { artists, addArtist } = useContext(ArtistContext);
useIonViewWillEnter(() => {
api.get('artists?manager=1'))
.then(function(response) {
artist = response.data.items[0];
addArtist(artist);
window.location.href = '/artist/' + artist.id + '/tabs';
})
.catch(error => console.log(error));
});
If i log inside the addArtist method, i can see that the artist object is valid, but then if i log again the artists array, it has the initial value.
I have tried different methods and syntax like class componenets with state, functional components with useState... So i'm guessing my syntax is correct but i'm missing some key concept about React.
Its my first project with React, so i'm probably missing something, but i can not figure it out even after reading the context documentation.
You need to store your state/context object to localStorage, sessionStorage, cookies or server since accessing a different url path refreshes the page.
This link may help you store your state when you refresh the page.
How to maintain state after a page refresh in React.js?
Just encountered this today. Took me 2 days to resolve this lol

React router redux fetch resources if needed on child routes

I'm struggling to make this work, this is a common pattern I think but I haven't been able to see an example for this, or at a solution.
Here is the current route I am working on
/app/services/10/
in app fetch the current user’s login information
in /services fetches the list of services the user has available to them
in /10 fetch fine grained details of Service 10
So the way I do it to populate the store with some data is:
App
import Services from './routes/Services'
export default (store) => ({
path: 'main',
getComponent (nextState, cb) {
require.ensure([], require => {
const App = require('./containers/AppContainer').default,
userActions = require('../store/user').actions
store.dispatch(userActions.fetch())
cb(null, App)
}, 'app')
},
childRoutes: [
Services(store)
]
})
Services
Now the problem lies within the childRoutes:
import { injectReducer } from '../../../../store/reducers'
import Manage from './routes/Manage'
export default (store) => ({
path: 'services',
getComponent (nextState, cb) {
require.ensure([], require => {
const Services = require('./containers/ServicesContainer').default
const actions = require('./modules/services').actions
const reducer = require('./modules/services').default
store.dispatch(actions.fetchAll())
injectReducer(store, { key: 'services', reducer })
cb(null, Services)
})
},
childRoutes: [
Manage(store)
]
})
As you can see the childRoute Services has a fetchAll() async request, that as you can imagine, needed some data from the store, specifically something from the user property in the store, like for example the userId or a token.
There wouldn't be a problem if I naturally navigate. But when I refresh, then the user prop hasn't been populated yet.
If you can't see how this is a problem, as part of my route:
app/services/10
The parameter 10 needed services from the store,
export default (store) => ({
path: ':id',
getComponent ({params: {id}}, cb) {
require.ensure([], require => {
const Manage = require('./containers/ManageContainer').default
const ServicesActions = require('../../modules/integrations').actions
store.dispatch(ServicesActions.selectService(id))
cb(null, Manage)
})
}
})
Where selectService is just a function that filters out state.services
The problem is services is fetched asynchronously and when you refresh that route, the store.dispatch gets executed even before the services in the store has completed and populated the store?
How do I approach this async issue?
TL;DR : Use the lifecycle hook of your component to fetch data when they need it, and conditionally render a "loading" state if the props are not ready. Or use HoC to encapsulate this behavior in a more reusable way.
Your problem is interesting because it's not relevant only for react-router, but for any react / redux application that need data to be fetched before rendering. We all struggled at least once with this issue : "where do I fetch the data ? How do I know if the data are loaded, etc.". That's the problem frameworks like Relay try to address. One very interesting thing about Relay is that you can define some data dependencies for your components in order to let them render only when their data are "valid". Otherwise, a "loading" state is rendered.
We generally achieve a similar result by fetching the needed data in the componentDidMount lifecycle method and conditionally render a spinner if the props are not "valid" yet.
In your specific case, I I understand it correctly, it can be generalized like that :
You hit the page /services/ with react-router
Your ServicesContainer loads all the services
You hit the page /services/10, since the services are already fetched there is no problem
You now decide to refresh but the page is rendered before the async fetching has finished hence your issue.
As suggested by the other answer, you can tackle this issue by fetching the data if needed and not rendering the services until the data are fetched. Something like this :
class Services extends React.Component {
componentDidMount() {
if (!this.props.areServicesFetched) {
this.props.fetchServices()
}
}
render() {
return this.props.areServicesFetched ? (
<ul>
{this.props.services.map(service => <Service key={service.id} {...service}/>)}
</ul>
) : <p>{'Loading...'}</p>
}
}
const ServicesContainer = connect(
(state) => ({
areServicesFetched: areServicesFetched(state) // it's a selector, not shown in this example
services: getServices(state) // it's also a selector returning the services array or an empty array
}),
(dispatch) => ({
fetchServices() {
dispatch(fetchServices()) // let's say fetchServices is the async action that fetch services
}
})
)(Services)
const Service = ({ id, name }) => (
<li>{name}</li>
)
That works great. You can stop reading this answer here if it's enough for you. If your want a better reusable way to do this, continue reading.
In this example, we are introducing some sort of "is my data valid to render or how can I make them valid otherwise ?" logic inside our component. What if we want to share this logic across different components ? As said by the doc :
In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible.
What we can understand here is that all our components should be pure, and not taking care of the others component, nor of the data flow (by data flow I mean, "is my data fetched ?", etc.). So let's rewrite our example with only pure components without worrying about data fetching for now :
const Services = ({ services }) => (
<ul>
{services.map(service => <Service key={service.id} {...service}/>)}
</ul>
)
Services.propTypes = {
services: React.PropTypes.arrayOf(React.PropTypes.shape({
id: React.PropTypes.string,
}))
}
const Service = ({ id, name }) => (
<li>{name}</li>
)
Service.propTypes = {
id: React.PropTypes.string,
name: React.PropTypes.string
}
Ok, so far we have our two pure components defining what props they need. That's it. Now, we need to put the "fetching data if needed when component did mount or render a loading state instead" somewhere. It's a perfect role for an Higher-Order Component or HoC.
Briefly speaking, an HoC lets you compose pure components together since they are nothing else than pure functions. An HoC is a function that takes a Component as an argument and return this Component wrapped with another one.
We want to keep separated the displaying of services and the logic to fetch them, because as I said earlier you may need the same logic of fetching the services in an another component. recompose is a little library that implements some very useful HoC for us. We're looking here at
lifecycle to add the componentDidMount lifecycle method
branch to apply a condition whether the services are fetched or not
renderComponent to render some <LoadingComponent> when services are fetching
mapProps to provide only the services prop to our <Services> component.
compose() utility to let us compose our HoC instead of nesting them
So let's build our ensureServices function which is responsible to :
connect the pure component to the redux store
Fetching the services if needed
Rendering a loading state if services are not yet received from the server
Rendering our component when the services are received
Here is an implementation :
const ensureServices = (PureComponent, LoadingComponent) => {
/* below code is taken from recompose doc https://github.com/acdlite/recompose/blob/master/docs/API.md#rendercomponent */
const identity = t => t
// `hasLoaded()` is a function that returns whether or not the component
// has all the props it needs
const spinnerWhileLoading = hasLoaded =>
branch(
hasLoaded,
identity, // Component => Component
renderComponent(LoadingComponent) // <LoadingComponent> is a React component
)
/* end code taken from recompose doc */
return connect(
(state) => ({
areAllServicesFetched: areAllServicesFetched(state), // some selector...
services: getServices(state) //some selector
}),
(dispatch) => ({
fetchServices: dispatch(fetchServices())
})
)(compose(
lifecycle({
componentDidMount() {
if (!this.props.areAllServicesFetched) {
this.props.fetchServices()
}
}
}),
spinnerWhileLoading(props => props.areAllServicesFetched),
mapProps(props => ({ services: props.services }))
)(PureComponent))
}
Now, wherever a component need the services from the store, we can just use it like this :
const Loading = () => <p>Loading...</p>
const ServicesContainer = ensureServices(Services, Loading)
Here, our <Services> component just display the services but if you have for example a <ServicesForm> component that need services to render an input for each services, we could just write something like :
const ServicesFormContainer = ensureServices(ServicesForm, Loading)
If you wan't to generalize this pattern, you could take a look to react-redux-pledge, a tiny library I own that handles this kind of data dependencies.
I've run into this quite a bit on the apps I've worked on. It seems like you're using React Router - if this is the case, you can take advantage of the onEnter/onChange hooks.
API Documentation is here: https://github.com/reactjs/react-router/blob/master/docs/API.md#onenternextstate-replace-callback
Instead of loading data in the async getComponent method, you can use the onEnter hook and use the callback parameter (just like you're doing with the getComponent) to indicate the react-router should block loading of this route until data is loaded.
Something like this could work, if you're using redux-thunk:
export default (store) => ({
path: ':id',
getComponent ({params: {id}}, cb) {
require.ensure([], require => {
const Manage = require('./containers/ManageContainer').default
const ServicesActions = require('../../modules/integrations').actions
cb(null, Manage)
})
},
onEnter: (nextState, replace, cb) => {
const actions = require('./modules/services').actions
const reducer = require('./modules/services').default
//fetch async data
store.dispatch(actions.fetchAll()).then(() => {
//after you've got the data, fire selectService method (assuming it is synchronous)
const ServicesActions = require('../../modules/integrations').actions
store.dispatch(ServicesActions.selectService(id))
cb()//this tells react-router we've loaded all data
})
}
})
I've found the pattern of loading data using the router hooks to be a pretty clean way to ensure all of the data needed for the component to render is there. It's also a great way to intercept unauthenticated users, if necessary.
An alternative approach would be to explicitly load the data in the componentDidMount method of the component.

Resources