Vue: UI only renders data when I save changes in VS Code - javascript

I am new to Vue and I am struggling with an extremely weird issue:
In my component, in the creates() function I fetch data and save them into my items Array. I display the content of the items with v-for as a paragraph.
Now here comes the weird thing: When I start the App, nothing shows, although I can see in the Vue dev-tools that the proper data is stored in the items array. When I refresh, same happens. Only when I make changes in my code and save it, I see the proper data being displayed in the UI.
Code:
WeeklyRanking.vue:
<script>
import { computed } from "vue"
import RankingItem from "./RankingItem.vue";
import {getUsersWithPoints} from "../../../data/data";
export default {
name: "WeeklyRanking",
data() {
return {
items: [],
};
},
created (){
getUsersWithPoints().then((users) => {
this.items = users;
console.log(users);
});
},
computed: {
sortedRankings() {
return [...this.items].sort((a, b) => b.points - a.points);
},
maxPoints() {
return 95;
}
},
components: { RankingItem },
}
</script>
<template>
<div class="weekly-ranking-container">
<h1>Wochen - Ranking</h1>
<div class="ranking-chart">
<p v-for="r in items"> {{r.username}} </p>
{{items}}
</div>
<button>
<router-link to="/taskDone">+ Aufgabe erledigt</router-link>
</button>
</div>
</template>
<style>
.weekly-ranking-container{
padding: 24px;
max-width: 400px;
min-width: 300px;
}
.ranking-chart{
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>
The console.log shows the following:
The Vue inspector show the data as follows:
What I have tried so far
Use fake data directly in the data() part - works. The UI shows the initial data that I mock here.
get fake data from data source with fake delay - works as well. Shows the proper data after the artificial delay.
The interesting thing in this case, is that the Array in the console log looks different:
I could not make sense of the difference between those two logs, especially because the Vue inspector displays it exactly the same. Also, the part with the code saving does not make sense to me.
Below I show the data call code. I use firebase with the modular api.
Thanks for your help!
Data call code:
async function getUsersWithPoints(){
// With the commented part it works
// const fakeUsers = [
// {username:"Marcelo", points:95, id:"PRirMre5IUeHP7BA08wh"},
// {username:"Sebse", points:80, id:"PRirMasdoiHP7BA08wh"},
// {username:"Simi", points:20, id:"yhaHRRxN7PFmfyHZqZS1"},
// ];
// await new Promise(r => setTimeout(r, 2000));
// console.log("FAKE USERS:");
// console.log(fakeUsers);
// return fakeUsers;
//with the below part it does not
let users = [];
const usersQuery = query(collection(db, `groups/${wgKey}/users`));
const usersSnap = await getDocs(usersQuery);
usersSnap.forEach(async(user)=>{
const tasksQuery = query(collection(db, `${user.ref.path}/tasks`));
const tasks = await getDocs(tasksQuery);
let points = 0;
tasks.forEach((task)=>{
points+=task.data().points;
});
users.push({
username: user.data().name,
points: points,
id: user.id
});
});
return users;
}
```

I can remember having an identical issue with an Vue 2 project some years ago. As far as I can see Valentin Rapp gave you the correct explanation already. The documentation also states:
Array Mutators
Vue is able to detect when an reactive array's mutation methods are called
Source: https://vuejs.org/guide/essentials/list.html#array-change-detection
One possible solution would be, to push your resulting elements to the reactive array, therefore triggering it's mutators.
// ...
created (){
getUsersWithPoints().then((users) => {
users.forEach(user => {
this.items.push(user)
})
console.log('Users:', this.items)
})
},
// ...
Object Mutators
Depending on the size of your users-array (> 10k) this approach could potentially lag/very slow on some devices. Another approach would be, to use the reactive mutators of an object by updating an objects prop. Please note, that I haven't used that with Vue 3 options API yet, so this could be faulty:
// ...
data() {
return {
user: {
items: [],
loaded: false, // can be used for some loading animation
},
}
},
created (){
getUsersWithPoints().then((users) => {
this.user.items = users
this.user.loaded = true
console.log('Users:', this.user.items)
})
},
// ...
Composition API
As stated before and propably lead to the confusion of Valentin Rapp is the useage of the options API in Vue 3. While this should be fully supported by Vue, with version 3 the composition API was implemented that could, besides other things, fix this problem for you:
// Vue 3 Component with Composition API — not a 1:1 approach!
// ...
setup() {
const items = ref([])
getUsersWithPoints().then((users) => {
users.forEach(user => {
this.items.push(user)
})
})
return {
items,
}
},
// ...
VS Code Saving Bug
This "bug" is more likely a confirmation for the problem stated above: mutators. Yet your given description is way to little to give you some detailed information here.
We don't know what your whole toolchain between "storing in vscode" to "bringing the new code to the browser" looks like. How does your code get transpiled? How is the file beeing watched? How is the hot reloading implemented?
Yet in general I think this will work like so:
File gets stored within your editor
Your toolchains watcher detects a changed file hash for one js file
Your toolchain transpiles your code
Your hot reload service will reload and inject the new code
In order to sync your current data within your browser instance and the default data within the hot-reloaded component will be replaced correctly with usage of vues mutators.
Your component therefor will detect the changes correctly. Your v-for get's rerendered and the data will be displayed correctly
But as I said, this is pure speculation and is purly depending on your setup.

So I tried multiple things to fix it:
Switching to Composition API
Using Array Mutators instead of replacing the Array
Using async - await instead of then(..)
Unfortunately, none of that fixed the problem. So I looked more deeply into the getUsersWithPoints function to find what I think was the mistake:
Using forEach with an async function.
Using Promise.all instead fixed the issue. Here is the code:
async function getUsersWithPoints(){
let usersExtended = new Array();
const usersQuery = query(collection(db, `groups/${wgKey}/users`));
const usersSnap = await getDocs(usersQuery);
await Promise.all(usersSnap.docs.map(async(user) => {
const tasksQuery = query(collection(db, `${user.ref.path}/tasks`));
const tasks = await getDocs(tasksQuery);
let points = 0;
tasks.forEach((task)=>{
points+=task.data().points;
});
usersExtended.push({
username: user.data().name,
points: points,
id: user.id
});
}));
return usersExtended;
}
Using array mutators seemed like a logical thing to do, although the docs say:
When working with non-mutating methods, we should replace the old array with the new one
...
You might think this will cause Vue to throw away the existing DOM and re-render the entire list - luckily, that is not the case. Vue implements some smart heuristics to maximize DOM element reuse, so replacing an array with another array containing overlapping objects is a very efficient operation.
source: https://vuejs.org/guide/essentials/list.html#array-change-detection
So as far as I understand, replacing the array will not re-render the entire DOM, but vue uses "smart heuristics" to render the replacing array very efficiently. So now that I fixed the data call I tried replacing and using Array Mutators and both worked.

In your created lifecycle hook you overwrite the reactive data array. This will not trigger an update as described here:
https://vuejs.org/guide/essentials/list.html#array-change-detection
You will need to push the data to the array. To modify and clear it use splice.
In Vue 3 you will not have to deal with this anymore.

Related

Vue child component not displaying dynamic data on first page load

Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource

Component not accessing context

i'm new to react please forgive me if i'm asking a dumb question.
The idea is to access the tweets array from context, find the matching tweet and then set it in the component's state to access the data.
However, the tweets array results empty even though i'm sure it's populated with tweets
const { tweets } = useContext(TweeetterContext)
const [tweet, setTweet] = useState({})
useEffect(() => {
loadData(match.params.id, tweets)
}, [])
const loadData = (id, tweets) => {
return tweets.filter(tweet => tweet.id == id)
}
return (stuff)
}
You are accessing context perfectly fine, and it would be good if you could share a code where you set tweets.
Independent of that, potential problem I might spot here is related to the useEffect function. You are using variables from external context (match.params.id and tweets), but you are not setting them as dependencies. Because of that your useEffect would be run only once at the initial creation of component.
The actual problem might be that tweets are set after this initial creation (there is some delay for setting correct value to the tweets, for example because of the network request).
Try using it like this, and see if it fixes the issue:
useEffect(() => {
loadData(match.params.id, tweets)
}, [match.params.id, tweets])
Also, not sure what your useEffect is actually doing, as it's not assigning the result anywhere, but I'm going to assume it's just removed for code snippet clarity.

Is it possible to use references as react component's prop or state?

I want to create react table component which values are derived from single array object. Is it possible to control the component from view side? My goal is that every user using this component in their web browsers share the same data via singleton view object.
Program modeling is like below.
Database - there are single database in server which contain extinct and independent values.
DataView - there are singleton View class which reflects Database's table and additional dependent data like (sum, average)
Table - I'll build react component which looks like table. And it will show View's data with supporting sorting, filtering, editing and deleting row(s) feature (and more). Also it dose not have actual data, only have reference of data from View(Via shallow copy -- This is my question, is it possible?)
My intentions are,
- When user changes value from table, it is queried to DB by View, and if succeed, View will refer updated data and change it's value to new value and notify to Table to redraw it's contents. -- I mean redraw, not updating value and redraw.
- When values in View are changed with DB interaction by user request, there are no need to update component's value cause the components actually dose not have values, only have references to values (Like C's pointer). So only View should do is just say to Component to redraw it's contents.
I heard that React's component prop should be immutable. (Otherwise, state is mutable) My goal is storing references to component's real value to it's props so that there are no additional operation for reflecting View's data into Table.
It is concept problems, and I wonder if it is possible. Since javascript dose not support pointer officially(Am I right?), I'm not sure if it is possible.
View class is like below,
const db_pool = require('instantiated-singleton-db-pool-interface')
class DataView {
constructor() {
this.sessions = ['user1', 'user2'] // Managing current user who see the table
this.data = [ // This is View's data
{id:1, name:'James', phone:'12345678', bank:2000, cash:300, total:2300,..},
{id:2, name:'Michael', phone:'56785678', bank:2500, cash:250, total:2300,..},
{id:3, name:'Tyson', phone:'23455432', bank:2000, cash:50, total:2300,..}
] // Note that 'total' is not in db, it is calculated --`dependent data`
}
notifySessionToUpdate(ids) {
// ids : list of data id need to be updated
this.sessions.forEach((session) => {
session.onNotifiedUpdateRow(ids) // Call each sessions's
})
}
requestUpdateRow(row, changed_value) {
// I didn't write async, exception related code in this function for simple to see.
update_result = db_pool.update('UPDATE myTable set bank=2500 where id=1')
if (update_result === 'fail') return; // Do Nothing
select_result = db_pool.select('SELECT * from myTable where id=1') // Retrieve updated single data which object scheme is identical with this.data's data
for (k in Object.keys(select_result)) {.ASSIGN_TO_row_IF_VALUE_ARE_DIFFERENT.} // I'm not sure if it is possible in shallow copy way either.
calc.reCalculateRow(row) // Return nothing just recalculate dependant value in this.data which is updated right above.
// Notify to session
this.notifySessionToUpdate([1]) // Each component will update table if user are singing id=1's data if not seeing, it will not. [1] means id:1 data.
return // Success
}
... // other View features
}
Regarding session part, I'm checking how to implement sessionizing(?) the each user and it's component who is communicating with server. So I cannot provide further codes about that. Sorry. I'm considering implementing another shallow copied UserView between React Component Table and DataView(And also I think it helps to do something with user contents infos like sorting preference and etc...)
Regarding DB code, it is class which nest it's pool and query interface.
My problem is that I'm not familiar with javascript. So I'm not sure shallow copy is actually implementable in all cases which I confront with.
I need to think about,
1. Dose javascript fully support shallowcopy in consistent way? I mean like pointer, guarantee check value is reference or not.
2. Dose react's component can be used like this way? Whether using props or state Can this be fullfilled?
Actually, I strongly it is not possible to do that. But I want to check your opinions. Seems it is so C language-like way of thinking.
Redraw mean re-render. You can expose setState() or dispatch() functions from Table component and call them on View level using refs:
function View() {
const ref = useRef();
const onDbResponse = data => ref.current.update(data);
return (
<Table ref={ ref } />
);
}
const Table = React.forwardRef((props, ref) => {
const [ data, setData ] = useState([]);
useImperativeHandler(ref, {
update: setData
});
...
});
Anyway i don't think it's a good practice to update like that. Why can't you just put your data in some global context and use there?
const Context = React.createContext({ value: null, query: () => {} });
const Provider = ({ children }) => {
const [ value, setValue ] = useState();
const query = useCallback(async (request) => {
setValue(await DB.request(request));
}, [ DB ]);
const context = { value, query };
return <Context.Provider value={ context }>{ children }</Context.Provider>;
}
const useDB = () => useContext(Context);
const View = () => {
const { request } = useDB();
request(...);
}
const Table = () => {
const { value } = useDB();
...
}

How to create a dynamic HTML table with Firebase?

I'm trying to create an HTML table that can be updated in real time with firebase as my users use my app. I'm really struggling on getting this to work, I don't really know where to start.
basically, all it needs to do is create a new table row and update its cells in real time as the user is interacting with my app.
Does anyone know of any good tutorials that could point me in the right direction? Examples of code on creating the HTML table would also be great! thanks.
UPDATE:
OK! thanks to Isaiah Lee, I went out and looked up some tutorials for React. I'm 90% there but there is one problem I can't seem to figure out...
I'm able to update my table with new rows dynamically as the users use my app, but I can't get them to fill up with data. I feel like I'm really close but my inexperience with React is holding me back here...
for some reason, this loop here doesn't populate the td's with any data
render() {
return (
<div className="App">
<h1>Talkeetna Numbers</h1>
<table id="numbers">
<tbody>
<tr>
<th>Driver</th>
<th>Coach</th>
<th>Time</th>
<th>Total People</th>
<th>AM Train</th>
<th>PM Train</th>
<th>MEX</th>
<th>Employees</th>
<th>Seats Left</th>
</tr>
{this.state.rows.map(row =>
<tr>
<td>{this.state.driver}</td>
<td>{this.state.coach}</td>
<td>{this.state.time}</td>
<td>{this.state.totalPeople}</td>
<td>{this.state.amTrain}</td>
<td>{this.state.pmTrain}</td>
<td>{this.state.THEMEX}</td>
<td>{this.state.Employees}</td>
<td>{this.state.seatsLeft}</td>
</tr>)}
</tbody>
</table>
</div>
);
}
Heres my components function
componentDidMount()
{
const logsRef = firebase.database().ref('logs');
logsRef.on('child_added', snap => {
this.addRow(snap.getKey());
//rows.push(snap.getKey());
console.log("adding key: ", snap.getKey());
});
console.log("loading rows...");
for(var rowKey = 0; rowKey < this.state.rows.length; rowKey++)
{
const root = firebase.database().ref().child('logs/' + rowKey);
const driverRef = root.child('Driver');
const coachRef = root.child('Coach');
const timeRef = root.child('Time');
const totalPeopleRef = root.child('Total People');
const AMTrainRef = root.child('AM Train');
const PMTrainRef = root.child('PM Train');
const MEXRef = root.child('MEX');
const EmployeesRef = root.child('Employees');
const seatsLeftRef = root.child('Seats Left');
//sync with DB in real time
driverRef.on('value', snap => {
this.setState({
driver: snap.val()
})
});
coachRef.on('value', snap => {
this.setState({
coach: snap.val()
})
});
timeRef.on('value', snap => {
this.setState({
time: snap.val()
})
});
totalPeopleRef.on('value', snap => {
this.setState({
totalPeople: snap.val()
})
});
AMTrainRef.on('value', snap => {
this.setState({
amTrain: snap.val()
})
});
PMTrainRef.on('value', snap => {
this.setState({
pmTrain: snap.val()
})
});
MEXRef.on('value', snap => {
this.setState({
THEMEX: snap.val()
})
});
EmployeesRef.on('value', snap => {
this.setState({
Employees: snap.val()
})
});
seatsLeftRef.on('value', snap => {
this.setState({
seatsLeft: snap.val()
})
});
}
}
Sounds like you should look into some frontend framework. In your particular use case, anything with 1-way binding will work (eg react). And of course, anything with 2-way binding will work as well (angular).
Egghead.io has really well done, bite-size tutorials that can get you running shortly: https://egghead.io/browse/frameworks
Edit:
Okay, it's been over a year since I've last been working with react, but I have some ideas that could help.
First, react is all about separating components and breaking things off into smaller bits. The idea is that you have some state change at the top level, those changes are propagated down into the nested components using props.
I'd recommend you have a top level component that listens for changes in Firebase (I havent worked with FB in a long time either), and then sends those down to components that render based on the data. I recommend reading this article written by Dan Abramov (creator of Redux), which discusses smart vs dumb components: https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
Again, I haven't used react in a while, so I'll use pseudocode to describe how you might want to design this.
Top Level Component
This component should be the only place your app is interacting with Firebase. You'll configure all the listeners here such that when new data is received, it simply propagates down to child components.
constructor() {
this.firebaseRef = firebase
.database()
.ref('logs');
this.state = {
logs: []
};
// when the table first loads, use the
// firebase ref to get all the current data
// and initialize/render the table. After this,
// you should only be listening for when a new row
// (or "log") is added. Pseudocode:
firebase.database().ref('logs')
.all()
.then(logs => {
// project all the logs into the model we need and set to
// component's row state
this.state.logs = logs.map(logMapper);
});
}
// takes in a log that you get from firebase and maps
// it to an object, modeled such that it contains all
// the information necessary for a single table row
logMapper(log) {
return {
driver: log.child('Driver'),
time: log.child('Time'),
...
};
}
componentDidMount() {
const logsRef = firebase
.database()
.ref('logs');
firebaseRef.on('child_added', log => {
this.state.logs.push(logMapper(log));
});
}
render() {
return (
<table id="numbers">
<thead>
<th>Driver</th>
<th>Coach</th>
<th>Time</th>
<th>Total People</th>
<th>AM Train</th>
<th>PM Train</th>
<th>MEX</th>
<th>Employees</th>
<th>Seats Left</th>
</thead>
<tbody>
{this.state.logs.map(row => <TableRow rowData="logs" />}
</tbody>
</table>
);
}
Row Component
You have a separate component that receives a log object through props and renders a single row.
render() {
return (
<tr>
<td>{this.props.rowData.driver}</td>
<td>{this.props.rowData.coach}</td>
<td>{this.props.rowData.time}</td>
<td>{this.props.rowData.totalPeople}</td>
<td>{this.props.rowData.amTrain}</td>
<td>{this.props.rowData.pmTrain}</td>
<td>{this.props.rowData.THEMEX}</td>
<td>{this.props.rowData.Employees}</td>
<td>{this.props.rowData.seatsLeft}</td>
</tr>
);
}
The above TableRow is an example of a dumb component. It doesn't hold any internal state, nor really modify anything. It just takes what it's given via props and renders what it's supposed to.
Again, you'll have to consider my examples above as pseudocode, it wouldn't work if you tried to copy and run it. But hopefully this gives you some insight into how to design components in react. Remember, "everything is a component".
Just a couple last notes:
Components are more useful the more generic they are. The ideal
scenario is that you create a component, and if you need something
similar later, you can just take the component you already created
and plug it in with slight modifications (or in the best case
scenario, no change at all!). So if you see yourself needing a bunch
of similar tables in your app, you'll want to further generalize my
example. For example, the firebase ref explicitly connects with the
"logs" collection, making this table component only useful for that one object type.
You'll want to make the parent component receive either a table
name, or an already initialized firebase reference, that it can use.
That way, the component is not tied down to a specific firebase
table. The mapping function should also be passed in, as the one in
my example is hardcoded to map a "log" object, and you may be
wanting to grab other types from firebase.
I learned a lot from reading and messing around, but honestly this one Udemy class I took really nailed down all the concepts for me: https://www.udemy.com/react-redux/
I'd highly recommend taking it, although you'd probably want to wait until Udemy has a site-wide sale where you can get the course for $10-20 bucks. I've taken several of his courses and they've all been well worth the money.
Good luck!

Idiomatic way to lazy-load with mobx

What is the current idiomatic way to lazy load properties when using MobX?
I've been struggling with this for a few days, and I haven't found any good examples since strict mode has become a thing. I like the idea of strict mode, but I'm starting to think lazy-loading is at odds with it (accessing, or observing a property should trigger the side effect of loading the data if it's not already there).
That's the crux of my question, but to see how I got here keep reading.
The basics of my current setup (without posting a ton of code):
React Component 1 (ListView):
componentWillMount
componentWillMount & componentWillReceiveProps - the component gets filter values from route params (react-router), saves it as an observable object on ListView, and tells the store to fetch 'proposals' based on it
Store.fetchProposals checks to see if that request has already been made (requests are stored in an ObservableMap, keys by serializing the filter object so two identical filters will return the same response object). It makes the request if it needs to and returns the observable response object that contains info on whether the request is finished or has errors.
ListView saves the observable response object as a property so it can display a loading or error indicator.
ListView has a computed property that calls Store.getProposals using the same filter object used to fetch
Store.getProposals is a transformer that takes a filter object, gets all proposals from an ObservableMap (keys on proposal.id), filters the list using the filter object and returns a Proposal[] (empty if nothing matched the filter, including if no proposals are loaded yet)
This all appears to work well.
The problem is that proposals have properties for client and clientId. Proposal.clientId is a string that's loaded with the proposal. I want to wait until client is actually accessed to tell the store to fetch it from the server (assuming it's not already in the store). In this case ListView happens to display the client name, so it should be loaded shortly after the Proposal is.
My closest I've gotten is setting up a autorun in the Proposal's constructor list this, but part of it is not reacting where I'm indending. (truncated to relevant sections):
#observable private clientId: string = '';
#observable private clientFilter: IClientFilter = null;
#observable client: Client = null;
constructor(sourceJson?: any) {
super(sourceJson);
if (sourceJson) {
this.mapFromJson(sourceJson);
}
//this one works. I'm turning the clientId string into an object for the getClients transformer
autorun(() => { runInAction(() => { this.clientFilter = { id: this.clientId }; }) });
autorun(() => {
runInAction(() => {
if (this.clientId && this.clientFilter) {
const clients = DataStore.getClients(this.clientFilter);
const response = DataStore.fetchClients(this.clientFilter);
if (response.finishedTime !== null && !response.hasErrors) {
this.client = clients[0] || null;
console.log('This is never called, but I should see a client here: %o', DataStore.getClients(this.clientFilter));
}
}
})
});
}
The response object is observable:
export class QueryRequest<T extends PersistentItem | Enum> {
#observable startTime: Date = new Date();
#observable finishedTime: Date = null;
#observable errors: (string | Error)[] = [];
#observable items: T[] = [];
#computed get hasErrors() { return this.errors.length > 0; }
#observable usedCache: boolean = false;
}
I'm getting the feeling I'm fighting the system, and setting up autoruns in the constructor doesn't seem ideal anyway. Anyone solve this pattern in a reasonable way? I'm open to suggestions on the whole thing if my setup looks crazy.
EDIT 1: removed #Mobx for clarity.
EDIT 2:
Trying to re-evaluate my situation, I (again) found the excellent lib mobx-utils, which has a lazyObservable function that may suite my needs. Currently it's looking like this:
client = lazyObservable((sink) => {
autorun('lazy fetching client', () => {
if (this.clientFilter && this.clientFilter.id) {
const request = DataStore.fetchClients(this.clientFilter);
if (request.finishedTime !== null && !request.hasErrors) {
sink(request.items[0]);
}
}
})
}, null);
This is working!
I think I need the autorun in there to update based on this objects clientId/clientFilter property (if this object is later assigned to a new client I'd want the lazyObservable to be updated). I don't mind a little boilerplate for lazy properties, but I'm, definitely open to suggestions there.
If this ends up being the way to go I'll also be looking at fromPromise from the same lib instead of my observable request object. Not sure because I'm keeping track of start time to check for staleness. Linking here in case someone else has not come across it:)
I've been using a different approach in my projects and I extracted it into a separate npm package: https://github.com/mdebbar/mobx-cache
Here's a quick example:
First, we need a React component to display the client info:
#observer
class ClientView extends React.Component {
render() {
const entry = clientCache.get(this.props.clientId)
if (entry.status !== 'success') {
// Return some kind of loading indicator here.
return <div>Still loading client...</div>
}
const clientInfo = entry.value
// Now you can render your UI based on clientInfo.
return (
<div>
<h2>{clientInfo.name}</h2>
</div>
)
}
}
Then, we need to setup the clientCache:
import MobxCache from "mobx-cache";
function fetchClient(id) {
// Use any fetching mechanism you like. Just make sure to return a promise.
}
const clientCache = new MobxCache(fetchClient)
That's all you need to do. MobxCache will automatically call fetchClient(id) when it's needed and will cache the data for you.

Categories