Using React UseEffect() to fetch data from GraphQL - javascript

I'm trying to fetch data from graphQL, and I know that by putting function into the react UseEffect(), I would be able to call the function once the data is updated and constructed.
However, I'm working on a chatroom, and the data does not appear on the screen:
import {
CREATE_MESSAGE_MUTATION,
MESSAGES_QUERY,
MESSAGE_SUBSCRIPTION,
} from "../graphql";
import { useQuery, useSubscription } from "#apollo/client";
import React, { useEffect } from "react";
import { Tag } from "antd";
const ChatBox = ({ me, friend, ...props }) => {
//me friend are strings
const chatBoxName = [me, friend].sort().join("_");
const { loading, error, data, subscribeToMore } = useQuery(MESSAGES_QUERY, {
variables: { name: chatBoxName },
});
useEffect(() => {
try {
subscribeToMore({
document: MESSAGE_SUBSCRIPTION,
variables: { name: chatBoxName },
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const newMessage = subscriptionData.data;
console.log("Subscribing more data: ", newMessage);
},
});
} catch (e) {
console.log("Error in subscription:", e);
}
}, [subscribeToMore]);
if (loading) return <p>loading ...</p>;
if (error) return <p>Error in frontend chatbox: {error}</p>;
return (
<div className="App-messages">
{console.log(data.chatboxs[0].messages)}
{data.chatboxs[0].messages.map(({ sender: { name }, body }) => {
<p className="App-message">
<Tag color="blue">{name}</Tag>
{body}
</p>;
})}
</div>
);
};
export default ChatBox;
After a small delay of loading ..., it turns to the <div className="App-messages"> with no messages inside. However, on the console I can clearly see the messages that I want to print.
What is the problem of the function in UseEffect()? I would be so appreciated if anyone can help .

{data.chatboxs[0].messages.map(({ sender: { name }, body }) => { // <- this part
<p className="App-message">
<Tag color="blue">{name}</Tag>
{body}
</p>;
})}
As a callback, you declared a function that does not return JSX elements.
Replace with this
{data.chatboxs[0].messages.map(({ sender: { name }, body }) => (
<p className="App-message">
<Tag color="blue">{name}</Tag>
{body}
</p>;
))}

Related

How to get the variables of id from another queried looped map's id

So I'm trying to get the userId from another queried looped map's id.
The error is:
Error: Rendered more hooks than during the previous render.
The error started showing when I added this inside the map:
const { data: { getUser } = {} } = useQuery(FETCH_USER_QUERY, {
variables: {
userId: something?.id,
},
});
on my component... This is my full component code:
export default function SomethingComponent() {
const { data: { getSomething } = {} } = useQuery(
FETCH_SOMETHING_QUERY
);
return (
<>
{getSomething?.map((something) => {
const { data: { getUser } = {} } = useQuery(FETCH_USER_QUERY, {
variables: {
userId: something?.id,
},
});
return (
<div>
<h1>{getUser.name}</h1>
{/* ... */}
{/* ... */}
{/* ... */}
{/* ... */}
{/* ... */}
{/* ... */}
</div>
);
})}
</>
);
}
And this is for the Query of Something:
const FETCH_SOMETHING_QUERY = gql`
{
getSomething {
id
}
}
`;
And for the query of the user:
const FETCH_USER_QUERY = gql`
query ($userId: ID!) {
getUser(userId: $userId) {
# ...
}
}
`;
Ive tried thinking on how to fix this myself but i dont know any other way to get the something.id without going inside the looped map. So i tried searching for the same error and they are about the hooks in the wrong order or place.
What you did was breaking the rules of hooks.
You'll need to utilize useApolloClient hook in order to manually execute the queries.
However, since we need to get the users individually, we can approach two ways for this.
The first way is to get the initial data first, then put it in useEffect hook with client extracted from useLazyQuery and then set the state one by one (which I think is a bit complicated):
The code below is just an idea. I won't guarantee that it will work if you copy+paste it.
// your previous code here
const [users, setUsers] = useState([])
const {
client
} = useApolloClient()
useEffect(() => {
if (getSomething.length > 0) {
getSomething.forEach(async({
id: userId
}) => {
const {
data: newUsers
} = await client.query({
query: FETCH_USER_QUERY,
variables: {
userId,
},
})
setUsers([...users, ...newUsers])
})
}
}, [getSomething])
2nd approach is to breakdown the component into smaller one with a fetch logic inside it.
export default function SomethingComponent() {
const { data: { getSomething } = {} } = useQuery(
FETCH_SOMETHING_QUERY
);
return (
<>
{getSometing.map((user) => <UserComponent userId={user.id} />)}
</>
);
}
// UserComponent.js
export default function UserComponent({ userId }) {
const { data: { user } = {} } = useQuery(
FETCH_USER_QUERY, { variables: { userId } }
);
return (
<>
{user?.name}
</>
);
}

Fetching with parameters in Javascript

As a quick summary, im trying to fetch from a URL and do so with 2 parameters.
I have no experience with javascript so i was trying this:
componentDidMount() {
$input = array("team" => {teamName}, "name" => {userPrincipalName});
fetch("http://localhost/openims/json.php?function=getDocuments&input=".urlencode(json_encode($input)))
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
files: result.files
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
This however does not seem to work. So my question would be: how do i succesfully pass the teamName and userPrincipalName from the context to the json_encode.
There is however 1 more problem with my code. I currently have two componentDidMounts, which are both using setState. The problem seems to be that whatever setState happens last, is the one that is being worked with, while the first setState is being completely overwritten. But i do need to do both the context setState AND the fetch to achieve my goal.
Here is my full code to give as clear an image as possible of what im doing:
import React from 'react';
import './App.css';
import * as microsoftTeams from "#microsoft/teams-js";
class Tab extends React.Component {
constructor(props){
super(props)
this.state = {
context: {}
}
}
componentDidMount(){
microsoftTeams.getContext((context, error) => {
this.setState({
context: context
});
});
}
componentDidMount() {
$input = array("team" => {teamName}, "name" => {userPrincipalName});
fetch("http://localhost/openims/json.php?function=getDocuments&input=".urlencode(json_encode($input)))
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
files: result.files
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { teamName, userPrincipalName } = this.state.context;
const { error, isLoaded, files } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{files.map(file => (
<li key={file.id}>
{file.name} {file.type}
<span id="user">Team: {teamName}, userPrincipalName: {userPrincipalName }</span>
</li>
))}
</ul>
);
}
}
}
export default Tab;
TL;DR
How do i use setState two times without problems? and how do i work the parameters teamName and userPrincipalName into my fetch?
Thank you!
If I understand correctly, what you need is backticks:
`http://localhost/openims/json.php?function=getDocuments&input=${userPrincipalName}`

useSelector does not rerender the component when state changes

I'm working on a news aggregator, and I have a Newsfeed component that maps through the relevant posts and creates a Post component for each one. There's a Sidebar component that shows the user the feeds they are subscribed to, and allows them to subscribe to new ones or unsubscribe from existing ones. What I'd like to happen is:
When a user adds a new feed, Newsfeed rerenders and now shows posts from the new feed.
When a user removes a feed, Newsfeed rerender and no longer shows posts from that particular feed.
As far as retrieving the correct posts - my backend takes care of that, and it works fine. The backend returns posts based on the feeds that the user is subscribed to. The problem is, when the user adds or removes a feed, the Newsfeed component does not rerender, and requires a page reload to show the updated feed. At the same time however, the Redux store IS updated, and I can see the state change every time via the Redux Dev Tools.
In Newsfeed, I'm using the useSelector hook to get a few different pieces of state, yet the component does not rerender when the state changes. I was under the impression that any component that used the useSelector hook would automatically be rerendered when that piece of state changed, but if that's not how the hook works then please correct me.
Newsfeed.tsx:
import React, { useState, useRef, useCallback } from "react";
import usePostFetch from "../hooks/usePostFetch";
import { Post } from "./Post";
import { Tag } from "./Tag";
import { Upvote } from "./Upvote";
import { getDate } from "../services/getDate";
import { useSelector } from "react-redux";
import { InitialState } from "../store/reducers/rootReducer";
export const Newsfeed = (props: any) => {
const userState = useSelector((state: InitialState) => {
return state.auth;
});
const { user } = userState;
const publisherState = useSelector((state: InitialState) => {
return state.publishers.publishers;
});
const [pageNumber, setPageNumber] = useState(1);
const { loading, error, posts, hasMore } = usePostFetch(pageNumber);
const observer: any = useRef();
const lastPostElementRef = useCallback(
(node) => {
if (loading) return;
if (observer && observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
console.log("Visible ");
setPageNumber((prevPageNumber) => prevPageNumber + 1);
}
});
if (node) observer.current.observe(node);
console.log(node);
},
[loading, hasMore]
);
return (
<div className="container mx-auto bg-gray-900" id="newsfeed">
<div className="object-center grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 mx-auto pb-6 pt-6">
{posts.map((post, index) => {
return (
<Post
key={post.id}
title={post.title}
url={post.url}
image={post.image}
category={post.category}
postId={post._id}
created={post.created}
publisher={post.publisher}
upvotes={post.upvotes}
/>
);
})}
</div>
<div>{loading && "Loading..."}</div>
<div>{error && "Error"}</div>
</div>
);
};
publisherActions.ts: (Relevant parts)
export const removeFeed = (allFeeds: any, feedName: any, userId: any) => async (
dispatch: Dispatch<PublisherDispatchTypes>
) => {
try {
axios({
method: "PUT",
url: "users/removepublisher",
params: { publisher: feedName, userId },
})
.then((res) => {
let newAllFeeds = allFeeds.filter((feed: any) => {
return feed.name.localeCompare(feedName) !== 0;
});
allFeeds = newAllFeeds;
console.log(`Feed was removed, ${res}`);
dispatch({
type: REMOVE_FEED_SUCCESS,
payload: allFeeds,
});
})
.catch((err) => {
console.log(`Error removing feed, ${err}`);
dispatch({
type: REMOVE_FEED_FAILURE,
});
});
} catch {
dispatch({ type: REMOVE_FEED_FAILURE });
console.log("Caught error while removing feed");
}
};
export const addFeed = (allFeeds: any, feed: any, userId: any) => async (
dispatch: Dispatch<PublisherDispatchTypes>
) => {
console.log("IN THE ADD_FEED FUNCTION");
try {
axios({
method: "PUT",
url: "users/addpublisher",
params: { publisher: feed, userId },
})
.then((res) => {
console.log(`Feed was added, ${res}`);
dispatch({
type: ADD_FEED_SUCCESS,
payload: {
name: feed.name,
url: feed.url,
image: feed.image,
},
});
})
.catch((err) => {
console.log(`Error adding feed, ${err}`);
dispatch({
type: ADD_FEED_FAILURE,
});
});
} catch {
dispatch({ type: ADD_FEED_FAILURE });
console.log("Caught error while adding feed");
}
publisherReducer.ts: (Relevant parts)
import { Reducer } from "react";
import {
PublisherDispatchTypes,
REMOVE_FEED_SUCCESS,
REMOVE_FEED_FAILURE,
ADD_FEED_SUCCESS,
ADD_FEED_FAILURE,
} from "../actions/publisherActionsTypes";
import { Publisher } from "../../../../shared/Publisher";
interface PublisherResponse {
publishers: Publisher[];
}
export interface PublisherState {
publishers: Publisher[] | undefined;
loadedUsersFeeds: boolean;
feedCount: number;
}
const defaultState: PublisherState = {
publishers: undefined,
loadedUsersFeeds: false,
feedCount: 0,
};
const publisherReducer = (
state: PublisherState = defaultState,
action: PublisherDispatchTypes
) => {
switch (action.type) {
case REMOVE_FEED_SUCCESS:
return {
...state,
publishers: action.payload,
};
case REMOVE_FEED_FAILURE:
return state;
case ADD_FEED_SUCCESS:
let pubs = state.publishers || [];
return {
...state,
publishers: [...pubs, action.payload],
};
case ADD_FEED_FAILURE:
return state;
default:
return state;
}
};
export default publisherReducer;

Retrieving state data from React

I have an input form and onSubmit, the input value will be rendered into a Checkbox. The data is being stored with MongoDB - I can see the input data I've typed using Robo 3T, so I know that part is working. However, the array is empty in the console and not being added to the Checkbox.
export const QUERY_ALL_CHORES = gql`
query chores {
chores {
_id
choreBody
}
}
`;
resolvers.js
addChore: async (parent, args, context) => {
// return console.log("chores: ", args.choreBody);
if (context.user) {
const chore = await Chore.create({
...args,
choreBody: args.choreBody,
});
await User.findByIdAndUpdate(
{ _id: context.user._id },
{ $push: { chores: chore._id } },
{ new: true }
);
return chore;
}
},
Then here is my AddChoreForm.js
export default function AddChore(props) {
const [choreBody, setBody] = useState("");
const [addChore] = useMutation(ADD_CHORE, {
update(cache, { data: { addChore } }) {
try {
const { chores } = cache.readQuery({ query: QUERY_ALL_CHORES });
cache.writeQuery({
query: QUERY_ALL_CHORES,
data: { chores: [addChore, ...chores] },
});
} catch (e) {
console.log(e);
}
// append new chore to the end of the array
const { me } = cache.readQuery({ query: QUERY_ME });
cache.writeQuery({
query: QUERY_ME,
data: { me: { ...me, chores: [...me.chores, addChore] } },
});
},
});
const handleFormSubmit = async (event) => {
event.preventDefault();
try {
// add chore to database
await addChore({
variables: { choreBody },
});
// clear form value
setBody("");
} catch (e) {
console.log(e);
}
};
return (
<Container>
<Form onSubmit={handleFormSubmit}>
<Form.TextArea
onChange={(event) => setBody(event.target.value)}
/>
<Button>
Add Chore
</Button>
</Form>
</Container>
);
}
Then the input data should be put into a Checkbox here, but when I check the console, the array is empty.
export default function ChoreList({ chores }) {
// get chore data
const { choreData } = useQuery(QUERY_ALL_CHORES);
const chores = choreData?.chores || [];
console.log("Chores: ", chores);
return (
<>
<Container chores={chores} >
{chores &&
chores.map((chore) => (
<div key={chore._id}>
<Form>
<Form.Field>
<List>
<List.Item>
<List.Content>
{/* {chore.choreBody} */}
<Checkbox label={chore.choreBody} />
</List.Content>
</List.Item>
</List>
</Form.Field>
</Form>
</div>
))}
</Container>
</>
);
}
Try passing the fetchPolicy: 'network-only' option with your useQuery hook to force fetch from DB rather than apollo cache.
From the docs:
By default your component will try to read from the cache first, and if the full data for your query is in the cache then Apollo simply returns the data from the cache.

React inline style with a variable from a function

I am trying to display the product getting the size it should be from a Json database. I am new to react so have tried a few ways and this is what I have been able to do.
I tried making a function (FontSize) that creates a variable (percentage) with the value I want before and then tried calling the function in the render in the tag with the product. I am getting no errors but the size of the paragraph tag is not changing.
This is my component.
import React, { Component } from 'react';
import { Loading } from './LoadingComponent';
const API = 'http://localhost:3000/products';
class Products extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
isLoading: false,
error: null,
};
}
componentDidMount() {
this.setState({ isLoading: true });
fetch(API)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ products: data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
FontSize = () => {
const { products } = this.state;
var percentage = products.size + 'px';
return percentage;
}
render() {
const Prods = () => {
return (
<div>
<div className="row">
<button onClick={this.sortPrice}>sort by price lower to higher</button>
<button onClick={this.sortSize}>sort by size small to big</button>
<button onClick={this.sortId}>sort by Id</button>
</div>
{products.map(product =>
<div className="row">
<div className="col-3">
<p> Price: ${(product.price/100).toFixed(2)}</p>
</div>
<div className="col-3">
<p style={{fontSize : this.FontSize()}} > {product.face}</p>
</div>
<div className="col-3">
<p>Date: {product.date} {this.time_ago}</p>
</div>
</div>
)}
<p>"~END OF CATALOG~"</p>
</div>
);
};
const { products, isLoading, error } = this.state;
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <Loading />;
}
return (
<Prods />
);
}
}
export default Products;
What I get in the console using console.log(products)
I think you need quotes around your style value to work properly.
With concatenation it would look like this for Example:
style={{gridTemplateRows: "repeat(" + artist.gallery_images.length + ", 100px)"}}
Another general example from React:
const divStyle = {
color: 'blue',
backgroundImage: 'url(' + imgUrl + ')',
};
function HelloWorldComponent() {
return <div style={divStyle}>Hello World!</div>;
}

Categories