Realm js run query async - reactjs

I'm using React-Native with Realm Database.
I tried to run read queries using the async/await pattern but it looks like it's always executing synchronously, freezing the UI since the query takes at least a couple of second till the rendering (due to a possible large amount of data stored).
This is the code I'm using (it's a copy and paste, but to give you an idea), am I missing something?
export default class CustomComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value:0
};
this.realm = this.buildRealm();
}
buildRealm() {
return new Realm({ schema: [EventSchema] });
}
componentWillMount() {
this.fetchData().then(result => { this.setState(value:result);});
}
async fetchData() {
var appState = await someMethod()
return appState;
}
someMethod() {
return new Promise(resolve => {
resolve(queryFromDB())
});
}
queryFromDB() {
// Returns a value fetched from Realm
let events = this.realm.objects("Event");
return events.length;
}
render() {
return (
<Text> {this.state.value} </Text>
);
}
}

Realm query is synchronous, there is no way to make it async.
Look like you are using realm with React Native, if this is the case, the best solution is to defer your realm query using the interaction manager
async componentDidMount() {
// loading objects from realm is a synchronous task that block the main
// process, in order not to freeze the ui, we trigger the load after
// the interactions and screen loading is done
this.getRealmDataHandle = InteractionManager.runAfterInteractions(() => this.getRealmData())
}
componentWillUnmount() {
if (this.getRealmDataHandle !== undefined) {
this.getRealmDataHandle.cancel()
}
}
First your component mount and render a loading screen, then when the ui rendering thread has finished it's work, the interaction manager will trigger the realm query. This way, the user is not experiencing too much of UI freezing.
I hope someday the realmJS team will provide an async version, but I doubt this will happen soon.

Have you tried fetching data on componentDidMount instead of willMount?
WillMount requires your functions to complete before mounting begins.
You can then add a loading UI state (spinner, sample text item etc).
Then perhaps make your state.value a default which then you update when your query is complete.

Related

MobX not hydrating in next.js state when fetching async data

I have a MobX store where I have a function doing an API call. It works fine it's getting the data but it doesn't update the already rendered page. I'm following this tutorial https://medium.com/#borisdedejski/next-js-mobx-and-typescript-boilerplate-for-beginners-9e28ac190f7d
My store looks like this
const isServer = typeof window === "undefined";
enableStaticRendering(isServer);
interface SerializedStore {
PageTitle: string;
content: string;
isOpen: boolean;
companiesDto: CompanyDto[],
companyCats: string[]
};
export class AwardStore {
PageTitle: string = 'Client Experience Awards';
companiesDto : CompanyDto[] = [];
companyCats: string[] = [];
loadingInitial: boolean = true
constructor() {
makeAutoObservable(this)
}
hydrate(serializedStore: SerializedStore) {
this.PageTitle = serializedStore.PageTitle != null ? serializedStore.PageTitle : "Client Experience Awards";
this.companyCats = serializedStore.companyCats != null ? serializedStore.companyCats : [];
this.companiesDto = serializedStore.companiesDto != null ? serializedStore.companiesDto : [];
}
changeTitle = (newTitle: string) => {
this.PageTitle = newTitle;
}
loadCompanies = async () => {
this.setLoadingInitial(true);
axios.get<CompanyDto[]>('MyAPICall')
.then((response) => {
runInAction(() => {
this.companiesDto = response.data.sort((a, b) => a.name.localeCompare(b.name));
response.data.map((company : CompanyDto) => {
if (company.categories !== null ) {
company.categories?.forEach(cat => {
this.addNewCateogry(cat)
})
}
})
console.log(this.companyCats);
this.setLoadingInitial(false);
})
})
.catch(errors => {
this.setLoadingInitial(false);
console.log('There was an error getting the data: ' + errors);
})
}
addNewCateogry = (cat : string) => {
this.companyCats.push(cat);
}
setLoadingInitial = (state: boolean) => {
this.loadingInitial = state;
}
}
export async function fetchInitialStoreState() {
// You can do anything to fetch initial store state
return {};
}
I'm trying to call the loadcompanies from the _app.js file. It calls it and I can see in the console.log the companies etc but the state doesn't update and I don't get to see the actual result. Here's the _app.js
class MyApp extends App {
constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = {
awardStore: new AwardStore()
};
this.state.awardStore.loadCompanies();
}
// Fetching serialized(JSON) store state
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
const initialStoreState = await fetchInitialStoreState();
return {
...appProps,
initialStoreState
};
}
// Hydrate serialized state to store
static getDerivedStateFromProps(props, state) {
state.awardStore.hydrate(props.initialStoreState);
return state;
}
render() {
const { Component, pageProps } = this.props;
return (
<Provider awardStore={this.state.awardStore}>
<Component {...pageProps} />
</Provider>
);
}
}
export default MyApp;
In the console.log I can see that this.companyCat is update but nothing is changed in the browser. Any ideas how I can do this? Thank you!
When you do SSR you can't load data through the constructor of the store because:
It's does not handle async stuff, so you can't really wait until the data is loaded
Store is created both on the server side and on the client too, so if theoretically constructor could work with async then it still would not make sense to do it here because it would load data twice, and with SSR you generally want to avoid this kind of situations, you want to load data once and reuse data, that was fetched on the server, on the client.
With Next.js the flow is quite simple:
On the server you load all the data that is needed, in your case it's loaded on the App level, but maybe in the future you might want to have loader for each page to load data more granularly. Overall it does not change the flow though
Once the data is loaded (through getInitialProps method or any other Next.js data fetching methods), you hydrate your stores and render the application on the server side and send html to the client, that's SSR
On the client the app is initialized again, though this time you don't want to load the data, but use the data which server already fetched and used. This data is provided through props to your page component (or in this case App component). So you grab the data and just hydrate the store (in this case it's done with getDerivedStateFromProps).
Based on that, everything you want to fetch should happen inside getInitialProps. And you already have fetchInitialStoreState method for that, so all you need to do is remove data fetching from store constructor and move it to fetchInitialStoreState and only return the data from it. This data will then go to the hydrate method of your store.
I've made a quick reproduction of your code here:
The huge downside if App.getInitialProps is that it runs on every page navigation, which is probably not what you want to do. I've added console.log("api call") and you can see in the console that it is logged every time you navigate to any other page, so the api will be called every time too, but you already have the data so it's kinda useless. So I recommend in the future to use more granular way of loading data, for example with Next.js getServerSideProps function instead (docs).
But the general flow won't change much anyway!
Calling awardStore.loadCompanies in the constructor of MyApp is problematic because the loadCompanies method is populating the store class. What you want is to hydrate the store with the companyCats data. Since server and client stores are distinct, you want to load the data you need on the server side i.e. fetchInitialStoreState (or load it from a page's getStaticProps/getServerSideProps method) so that you can pass it into the hydrate store method from page/app props.
Note loadCompanies is async so it'll be [] when getDerivedStateFromProps is called so there's nothing to hydrate. For your existing hydrate method to work you need initialStoreState to be something like the fetchInitialStoreState method below. Alternatively if it's fetched on the page level, the hydrate may be closer to initialData?.pageProps?.companyCats
It's common to see the store hydration as needed for each page though it's still valid to call loadCompanies() from the client side. There's a lot I didn't get a chance to touch on but hopefully this was somewhat helpful.
export const fetchInitialStoreState = async() => {
let companyCats = [];
try {
const response = await axios.get < CompanyDto[] > ('MyAPICall')
response.data.map((company: CompanyDto) => {
if (Array.isArray(company.categories) && company.categories.length > 0) {
companyCats.push(...company.categories)
}
})
} catch (error) {
// Uh oh...
}
return {
serializedStore: {
companyCats,
// PageTitle/etc
}
}
}

Cannot setState() with Firebase database listener

I'm trying to build a mini-game with rooms. When I try to retrieve room information from my firebase realtime database, I used database.ref().on() function to listen on any data changes and setting my state according to the change. Here is what the code looks like:
export default class Room extends Component {
state = {
room: null,
username: "sdff",
};
componentWillMount() {
const roomId = this.props.location.pathname;
app()
.database()
.ref(`/rooms${roomId}`)
.on("value", (snapshot) => {
const data = snapshot.val();
console.log(data);
this.setState({ room: data });
this.setState({ room: "hello" });
});
}
When I do console.log(data), I actually do see that data contains all information that I want. However, this.setState is not working, which causes the page failing to render since lots of the information from state is used in render(). Even if I set room to "hello", as shown in the last statement of the code snippet, it still fails to set state. I'm wondering if it's because render() is called before the data is successfully retrieved? If so, how can I fix this? Thanks in advance.
I think you need componentDidMount() instead.
As React official docs say:
This method is a good place to set up any subscriptions
Also, don't forget to end the subscription on componentWillUnmount()

How to structure a React app to pass values returned from a child's function

I'm building a react app that I want to seperate into three layers, and am having a little trouble wrapping my head around how they should be structured and in what hierarchy to best support React practices.
The three layers are service (only api calls), business logic, and UI. My goal here is to allow the service and business layers to be reusable and not be tied to specific UI components.
So in my mind, the structure of my program should be UI > (is the parent of) Business logic > (is the parent of) the service layer. However, I'm finding it difficult to see how this approach makes sense in react, so I'm wondering if it's the right idea. My issue is that as I understand it, parents pass down props to children. However, I need my service layer to pass objects to my business logic so it can do it's thing and effeciently hand off to the UI for display.
Here's a trimmed version of my service layer:
interface State {
queues: any
}
#observer
class DeadletterService extends Component<Props, State> {
#observable deadletterQueue: [];
#observable queues: [];
constructor(props: any) {
super(props);
this.state = {
queues: []
};
this.queues = this.getQueues();
this.deadletterQueue = this.getByQueue("urlSlug");
}
// returns all queues as a JSON array
getQueues(): any {
let url = "url";
fetch(url)
.then(res => res.json())
.then(data => {
return data;
});
}
// given the name of a queue, returns the queue's deadletters as a JSON array
getByQueue(id: string): any {
let url = "url/{id}";
fetch(url)
.then(res => res.json())
.then(data => {
return data;
});
return
}
render() {
return (
<div>
<DeadletterLogic testQueues={this.queues} />
</div>
);
}
}
And here's a trimmed version of my logic layer:
interface Props {
testQueues: any;
}
interface State {
queues: any
}
#observer
class DeadletterLogic extends Component<Props, State> {
constructor(props: any) {
super(props);
this.state = {
queues: []
};
this.getQueues();
}
// here is where the business logic will be done
getQueues() {
// in order to gets a list of queues here, a fetch call must be made in the service layer to the MessageQueueAPI
}
componentDidMount() {
// when passing from a parent, I'm not sure if this needs to live here or if it can live in getQueues to update when the fetch is finished.
this.setState({ queues: this.props.testQueues });
}
render() {
return (
<div>
{this.props.testQueues}
</div>
);
}
}
What's the best way to get the values returned from an api call in my service layer to my business layer in React? Because it's an async fetch call, I also need my business layer to update after I actually get data back.
Cheers!
I'm not sure why are you using your service and business layer as components? As you've said
My goal here is to allow the service and business layers to be
reusable and not be tied to specific UI components.
That means that they should not be related to any UI meaning those layers could be used in angular, react-native or any other UI layer implementation.
So you could have api layer (service):
class DeadletterService {
async getQueues(filter) {
const response = await fetch(...);
return await response.json();
}
// other api calls
}
const service = new DeadletterService();
export default service;
As you can see service just request and return data (no state) based on some input (for example filter, id or something else).
Then you have business layer where you fetch data from services, do whatever transformations on data you want and store that data in some kind of state which will be used on the UI layer.
import deadLetterService from './deadLetterService';
class DeadletterStore {
#observable queues = [];
#action
async getQueues() {
const queues = await deadLetterService.getQueues();
// do some mapping if needed
this.queues = queues;
}
}
Now you can use that store inside your UI - since you are using MobX you can make use of inject function:
#inject('deadLetterStore');
class DeadletterPage extends React.Component {
componentDidMount() {
this.props.deadLetterStore.getQueues();
}
render() {
return (
<div>
{this.props.deadLetterStore.queues.map((queue) => <span>{queue.name}</span>)}
</div>
)
}
}
Please note that this code is not tested - its just to point out structure design.

How to call a function right before leaving a component?

I am kind of new to react and I am developing a Frontend for a REST-API.
My Frontend for a REST-API is organized in 5 Sites(Components) which are routed with react-router-dom.
Every time I enter a Site, the ComponentDidLoad dispatches an action, which in turn calls the API in my reducer.
export function getData(pURL, ...pParams){
return (dispatch) => {
axios.get(pURL, {pParams})
.then(result => {
dispatch(getDataSuccess(result.data))
})
.catch(error => {
dispatch(getDataFailure(error))
})
}
}
One of my Main sites looks as follows
class Overview extends Component {
componentDidMount(){
this.props.getData(MyURLHere);
}
render(){
return(
<div>
{this.props.hasError ? "Error while pulling data from server " : ""}
{/* isLoading comes from Store. true = API-Call in Progress */}
{this.props.isLoading ? "Loading Data from server" : this.buildTable()}
</div>
)
}
}
let mapStateToProps = state => {
hasError: state.api.hasError,
isLoading: state.api.isLoading,
companies: state.api.fetched
}
let mapDispatchToProps = {
//getData()
}
export default connect(mapStateToProps, mapDispatchToProps)(Overview);
state.api.fetched is my store-variable I store data from every API-Call in.
this.buildTable() just maps over the data and creates a simple HTML-Table
My Problem is that I got the store-state variable isLoading set to truein my initialState.
But when I move to another site and then back to this one, it automatically grabs data from state.api.fetched and tries to this.buildTable() with it, because isLoading is not true. Which ends in a "is not a function" error, because there is still old, fetched data from another site in it.
My Solution would be to always call a function when leaving a component(site) that resets my store to it's initialState
const initialStateAPI = {
isLoading: true
}
or directly set isLoading to true, in Order to avoid my site trying to use data from old fetches.
Is there a way to do so?
I hope that I provided enough information, if not please let me know.
If you want to call the function when leaving a component you can use componentWillUnmount function. Read more about React Lifecycle methods.

What is a best way to get value from AsyncStorage before whole app load

I have App.js file and it is root of my application (both ios and android reference to it).
I have a value that I keep in AsyncStorage that I need before app.js render method is called.
Problem is that as it is async it late and I can't get that value.
class App extends React.Component {
constructor(props) {
super(props);
this.init()
}
async init() {
try {
const value = await AsyncStorage.getItem('#myPoorValue:key');
if (value !== null){
...
}
} catch (error) {}
}
}
...
render (...
I hope that I explained good what is my issue here.
I know that there is no way to get it synchronous (I would like that) but don't know what to do in this situation.
To explain it a bit better I use I18n and I manually set I18n.locale to some value and other components get default value before I set it manually.
Just to note I also use redux and I pass selected value to it.
try the following:
...
constructor(props) {
super(props)
this state = {
isLoading: true
}
}
async componentDidMount() {
await this.init()
// you might want to do the I18N setup here
this.setState({
isLoading: false
})
}
async init() {
const value = await AsyncStorage.getItem('#myPoorValue:key')
...
}
...
the thing is that init() returns a promise and you need to wait until it gets resolved. That's when await comes to rescue.
you'll also need to set up some loader that will be there on first render, and toggle the state to replace it with actual markup after the AsyncStorage value has been fetched. I've put it in the code, but you might want to trigger a redux action instead, depending on your setup.

Resources