I have an async function that gets data from my Supabase database, that when called, returns a promise with the correct data that I have queried, but when I try to call this function in a React component, I don't know how to extract the data from the Promise and just get the string that I queried.
I understand that you can not get the result of a promise in the same scope as you call it, but I'm not sure how I would get around this.
My code:
export async function getUserValue(uuid, value) {
const { data, error } = await supabase
.from('users')
.select('username').eq("id", "8f1693d3-c6d9-434c-9eb7-90882ea6ef28"); // hard coded values for testing purposes
return data;
}
Where I call it:
...
async function Sidebar(props) {
console.log(getUserValue("", ""))
return (
<div className={"sidebar"}>
<div className="sidebar-main">
<img className={"sidebar-main-picture"} src={profile_picture} alt="pfp"/>
<p className={"sidebar-main-name"}>Test</p>
...
Result
The way to store data in React components is to define and set state.
The correct place to handle side-effects like async response data is in an effect hook
import { useEffect, useState } from "react";
function Sidebar(props) {
const [ user, setUser ] = useState(null); // initial value
useEffect(() => {
getUserValue("", "")
.then(users => {
setUser(users[0]) // your response is an array, extract the first value
})
.catch(console.error)
}, []); // empty array means run this once on mount
return user && ( // only display if `user` is set
<p>Hello, { user.username }</p> {/* just an example */}
);
}
I feel like this has definitely been asked and answered before but I couldn't find an applicable duplicate. Happy to remove this or mark it Community Wiki if somebody can link an existing post.
Related
I am trying to verify if the user is inside that list that I capture by axios, the issue is that I have used the FILTER option but it always returns undefined or [], being that if the user exists in that array.
I can't think what else to do, because I validate if it is by console.log() the variable with which I ask and if it brings data.
created() {
this.getStagesDefault()
this.getSalesman()
this.getStagesAmountByUser()
},
methods: {
async getSalesman(){
const { data } = await axios.get('salesman')
this.employees = data.data
},
getStagesAmountByUser(){
console.log(this.user['id'])
var objectUser = this.employees.filter(elem => {
return elem.id === this.user['id']
})
console.log(objectUser)
},
Console
Vue data
The method getSalesman is asynchronous, meaning that getStagesAmountByUser will start executing before getSalesman finishes.
Two ways to fix the problem:
Await the getSalesman method, but you have to make the created method async as well. Change the code as follows:
async created() {
this.getStagesDefault()
await this.getSalesman()
this.getStagesAmountByUser()
}
Attach a .then to the getSalesman function, and start the next one inside the .then. Change the code as follows:
created() {
this.getStagesDefault()
this.getSalesman().then(() => this.getStagesAmountByUser())
}
getSalesman is an async method. At the time of the filter, the array being filtered is still empty.
this.getSalesman() // this runs later
this.getStagesAmountByUser() // this runs right away
Have the methods run sequentially by awaiting the async method:
await this.getSalesman()
this.getStagesAmountByUser()
You can avoid the inefficient clientside filtering if you pass the id to the backend and only select by that id.
Additionally, created only gets called once unless you destroy the component which is also inefficient, so watch when user.id changes then call your method again.
Plus don't forget you must wrap any async code in a try/catch else you will get uncaught errors when a user/salesman is not found etc, you can replace console.error then with something which tells the user the error.
{
data: () => ({
employee: {}
}),
watch: {
'user.id' (v) {
if (v) this.getEmployee()
}
},
created() {
this.getEmployee()
},
methods: {
getEmployee() {
if (typeof this.user.id === 'undefined') return
try {
const {
data
} = await axios.get(`salesman/${this.user.id}`)
this.employee = data.data
} catch (e) {
console.error(e)
}
}
}
}
I need data from 2 different APIs, both support pagination.
How can I display the joined data in a table in a performant way?
I need to join on their id, but one API returns less data than the other, so I cannot simply match 1 by 1. I need to implement a filter.
Is the only way to brute force map data source A to B?
If the data comes from two different APIs and you are making to separate requests you have a number of options. My personal preference is to have state in your controller in which you map each response by id and then you can select the additional data by id:
import React, { useState, useEffect } from 'react';
import { keyBy } from 'lodash';
function TableComponent(props) {
// Destructure your props...I'm assuming you pass some id into fetch data
const { id } = props;
// State
const [tableData, setTableData] = useState([]);
// Load data when id changes
useEffect(() => {
fetchData()
}, [id]);
async function fetchData() {
// Get your data from each source
const apiData_A = await fetchDataFromAPI_A(id);
const apiData_B = await fetchDataFromAPI_B(id);
// Key each data set by result ids
const resultsMappedById_A = keyBy(apiData_A, 'id');
const resultsMappedById_B = keyBy(apiData_B, 'id');
// Combine data into a single set
// this assumes your getting same results from each api
const combinedDataSet = Object.keys(resultsMappedById_A)
.reduce((acc, key) => {
// Destructure results together, merging objects
acc.push({
...resultsMappedById_A[key],
...resultsMappedById_B[key]
});
return acc;
}, []);
setTableData(combinedDataSet);
}
async function fetchDataFromAPI_A(id) {
// Fetch your data and return results
}
async function fetchDataFromAPI_A(id) {
// Fetch your data and return results
}
function renderTableRow(data) {
return (
<tr>
<td>{data.id}</td>
<td>{data.apiAProp}</td>
<td>{data.apiBProp}</td>
</tr>
);
}
return (
<table>
{ tableDataSet.map(renderTableRow) }
</table>
);
}
Note, there are probably more efficient ways to do this depending on how you're fetching data and what the responses hold, but the concept provided here should do the trick assuming my assumptions are correct based on the information you have provided.
There is an object product which has object manufacturer as its field. After fetching manufacturers from my server, I reassign product's field manufacturer with a new data (for example, fetched manufacturer has additional object avatar as its field).
async componentDidMount() {
strapi.setToken(this.state.user.jwt);
const products = await strapi.getEntries('products');
products.forEach(async product => {
const manufacturer = await strapi.getEntry('manufacturers', product.manufacturer.id); //fetched manufacturers has additional field "avatar"
Object.assign(product.manufacturer, manufacturer);
});
console.log(products); // product.manufacturer.avatar is not null
this.setState({
products
});
Then I'm trying to display avatar in React render().
render() {
if (!this.state.products) return null;
return(
{this.state.products ? this.state.products.map(product => (
// and it says that avatar is null
<img src={product.manufacturer.avatar.url} />
// displays manufacturer with avatar object
{console.log(product.manufacturer)}
// says null!
{console.log(product.manufacturer.avatar)}
))}
)
}
Also when I check state with React Developer Tools, product's field manufacturer has object avatar and it isn't null.
UPDATE:
Thanks to Sergey Suslov
strapi.setToken(user.jwt);
const fetchedProducts = await strapi.getEntries('products');
const promices = fetchedProducts.map(async fetchedProduct => {
const manufacturer = await strapi.getEntry('manufacturers', fetchedProduct.manufacturer.id);
const product = { ...fetchedProduct, manufacturer };
return product;
});
Promise.all(promices).then(products =>
this.setState({
products
})
);
You are assigning with the assign method that does not mutate source object, it returns new object as a result, check this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign.
You need to do semthing like this:
product = {
...product,
manufacturer: your assign statment
}
Main reason is that your callback function in foreach is asynchronous, js does not wait for all foreach function calls to completed, it keep on running, couse async word, try to use Promise for this, or better try to use actions for async requests, thank librariy or sagas.
It's because at first render, you do not have the avatar value. These values are fetched after the first render (componentDidMount).
You need to add a test to take that first render into account.
Also, the reason why your console.log is not consistent, is because you are overwriting the value of product, to be more immutable, you should .map instead of forEach, and return a copy of your product instead of modifying the existing one.
How I would write it:
constructor(props){
super(props);
this.state = {
products: []
}
}
render() {
return this.state.products.map(product => {
const {
avatar = {}
} = product.manufacturer;
return (
<img src={avatar.url} />
);
});
}
I have a props which includes users array and messages array. Each user and message is related through a corresponding ID.
I want to match these IDs so I can render messages with the user who posted it.
The issue is nothing is rendering on screen but in the console.log(), I can see the results. Please, can someone shed some light into this.
Also I'm not sure if I'm doing this the right way, so if you can show me a better way, I'll be glad.
Thanks
React:
import React, { Component } from 'react';
class Messages extends Component {
constructor(props) {
super(props);
}
render() {
let self = this;
return (
<ul>
{
this.props.messages.map((message, msg_key) => {
self.props.members.forEach((member) => {
if (member.id === message.userId) {
console.log('inside foreach message.userId', message.userId); // this shows in console.
console.log('inside foreach member.id', member.id); // this shows in console.
return <p>member.id</p> // nothing shows on screen.
}
})
})
}
</ul>
);
}
}
export default Messages;
forEach has no side-effect. Use map
return (
<ul>
{
this.props.messages.map((message, msg_key) => {
return self.props.members.map((member) => { // <-- this line
if (member.id === message.userId) {
console.log('inside foreach message.userId', message.userId); // this shows in console.
console.log('inside foreach member.id', member.id); // this shows in console.
return <p>member.id</p> // nothing shows on screen.
}
})
})
}
</ul>
)
what I mean by "side-effect" is that it neither returns anything nor modifies the original array - map returns a modified array.
remember to supply a key prop when returning an array.
edit
Ah, actually ^ doesn't do what you want.
Array has a nice method to accomplish what you're attempting, find
return (
<ul>
{
this.props.messages.map((message, msg_key) => {
const member = self.props.members.find((member) => member.id === message.userId)
return <p>{member.id}</p>
})
}
</ul>
)
edit #2
well... it actually does work, but it's not pretty. React won't render null in arrays, so it'll skip the ones where the condition wasn't true...
you should redefine your data structure. What you currently have is not going to be performant.
instead of an array of members.. you should have an object key'd by the member id. This is so you can lookup a member by their id. Anytime you have information that is going to be used for other fields of data you should have a key value storage setup key'd by the unique identifier that maps to the other pieces of information. Heres an example
members: {
1: { id: 1, name: 'foo'},
2: { id: 2, name: 'bar'}
}
when you define a data structure like this you can easily look up members for messages.
render (){
const elems = [];
this.props.messages.forEach((message, msg_key) => {
const member = this.props.members[message.userId];
if (member) {
elems.push(
<li key={msg_key}>
<p>{message.title}</p> /* or whatever you want from the message */
<p>{member.id}</p>
</li>
);
}
})
return (
<ul>
{elems}
</ul>
)
}
I think you might messed up some of the attributes inside the array by having 2 loops at 1 time (map, and forEach).
Try have a helper function to map through (instead of forEach) the member.id attributes. So just call {this.renderMemberList} inside the render().
Do you have a JSFiddle set up? it would be easier
I have a mutation like
mutation deleteRecord($id: ID) {
deleteRecord(id: $id) {
id
}
}
and in another location I have a list of elements.
Is there something better I could return from the server, and how should I update the list?
More generally, what is best practice for handling deletes in apollo/graphql?
I am not sure it is good practise style but here is how I handle the deletion of an item in react-apollo with updateQueries:
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'react-addons-update';
import _ from 'underscore';
const SceneCollectionsQuery = gql `
query SceneCollections {
myScenes: selectedScenes (excludeOwner: false, first: 24) {
edges {
node {
...SceneCollectionScene
}
}
}
}`;
const DeleteSceneMutation = gql `
mutation DeleteScene($sceneId: String!) {
deleteScene(sceneId: $sceneId) {
ok
scene {
id
active
}
}
}`;
const SceneModifierWithStateAndData = compose(
...,
graphql(DeleteSceneMutation, {
props: ({ mutate }) => ({
deleteScene: (sceneId) => mutate({
variables: { sceneId },
updateQueries: {
SceneCollections: (prev, { mutationResult }) => {
const myScenesList = prev.myScenes.edges.map((item) => item.node);
const deleteIndex = _.findIndex(myScenesList, (item) => item.id === sceneId);
if (deleteIndex < 0) {
return prev;
}
return update(prev, {
myScenes: {
edges: {
$splice: [[deleteIndex, 1]]
}
}
});
}
}
})
})
})
)(SceneModifierWithState);
Here is a similar solution that works without underscore.js. It is tested with react-apollo in version 2.1.1. and creates a component for a delete-button:
import React from "react";
import { Mutation } from "react-apollo";
const GET_TODOS = gql`
{
allTodos {
id
name
}
}
`;
const DELETE_TODO = gql`
mutation deleteTodo(
$id: ID!
) {
deleteTodo(
id: $id
) {
id
}
}
`;
const DeleteTodo = ({id}) => {
return (
<Mutation
mutation={DELETE_TODO}
update={(cache, { data: { deleteTodo } }) => {
const { allTodos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { allTodos: allTodos.filter(e => e.id !== id)}
});
}}
>
{(deleteTodo, { data }) => (
<button
onClick={e => {
deleteTodo({
variables: {
id
}
});
}}
>Delete</button>
)}
</Mutation>
);
};
export default DeleteTodo;
All those answers assume query-oriented cache management.
What if I remove user with id 1 and this user is referenced in 20 queries across the entire app? Reading answers above, I'd have to assume I will have to write code to update the cache of all of them. This would be terrible in long-term maintainability of the codebase and would make any refactoring a nightmare.
The best solution in my opinion would be something like apolloClient.removeItem({__typeName: "User", id: "1"}) that would:
replace any direct reference to this object in cache to null
filter out this item in any [User] list in any query
But it doesn't exist (yet)
It might be great idea, or it could be even worse (eg. it might break pagination)
There is interesting discussion about it: https://github.com/apollographql/apollo-client/issues/899
I would be careful with those manual query updates. It looks appetizing at first, but it won't if your app will grow. At least create a solid abstraction layer at top of it eg:
next to every query you define (eg. in the same file) - define function that clens it properly eg
const MY_QUERY = gql``;
// it's local 'cleaner' - relatively easy to maintain as you can require proper cleaner updates during code review when query will change
export function removeUserFromMyQuery(apolloClient, userId) {
// clean here
}
and then, collect all those updates and call them all in final update
function handleUserDeleted(userId, client) {
removeUserFromMyQuery(userId, client)
removeUserFromSearchQuery(userId, client)
removeIdFrom20MoreQueries(userId, client)
}
For Apollo v3 this works for me:
const [deleteExpressHelp] = useDeleteExpressHelpMutation({
update: (cache, {data}) => {
cache.evict({
id: cache.identify({
__typename: 'express_help',
id: data?.delete_express_help_by_pk?.id,
}),
});
},
});
From the new docs:
Filtering dangling references out of a cached array field (like the Deity.offspring example above) is so common that Apollo Client performs this filtering automatically for array fields that don't define a read function.
Personally, I return an int which represents the number of items deleted. Then I use the updateQueries to remove the document(s) from the cache.
I have faced the same issue choosing the appropriate return type for such mutations when the rest API associated with the mutation could return http 204, 404 or 500.
Defining and arbitrary type and then return null (types are nullable by default) does not seem right because you don't know what happened, meaning if it was successful or not.
Returning a boolean solves that issue, you know if the mutation worked or not, but you lack some information in case it didn't work, like a better error message that you could show on FE, for example, if we got a 404 we can return "Not found".
Returning a custom type feels a bit forced because it is not actually a type of your schema or business logic, it just serves to fix a "communication issue" between rest and Graphql.
I ended up returning a string. I can return the resource ID/UUID or simply "ok" in case of success and return an error message in case of error.
Not sure if this is a good practice or Graphql idiomatic.