How to get running Gatsby page query data with React Context API? - javascript

I'm working on a site where I have a gallery and custom build lightbox. Currently, I'm querying my data with a page query, however, I also use them in other components to display the right images and changing states. It is easier for me to store states in Context API as my data flow both-ways (I need global state) and to avoid props drilling as well.
I've setup my context.provider in gatsby-ssr.js and gatsby-browser.js like this:
const React = require("react");
const { PhotosContextProvider } = require("./src/contexts/photosContext");
exports.wrapRootElement = ({ element }) => {
return <PhotosContextProvider>{element}</PhotosContextProvider>;
};
I've followed official gatsby documentation for wrapping my root component into context provider.
Gallery.js here I fetch my data and set them into global state:
import { usePhotosContext } from "../contexts/photosContext";
const Test = ({ data }) => {
const { contextData, setContextData } = usePhotosContext();
useEffect(() => {
setContextData(data);
}, [data]);
return (
<div>
<h1>hey from test site</h1>
{contextData.allStrapiCategory.allCategories.map((item) => (
<p>{item.name}</p>
))}
<OtherNestedComponents />
</div>
);
};
export const getData = graphql`
query TestQuery {
allStrapiCategory(sort: { fields: name }) {
allCategories: nodes {
name
}
}
}
`;
export default Test;
NOTE: This is just a test query for simplicity
I've double-checked if I get the data and for typos, and everything works, but the problem occurs when I try to render them out. I get type error undefined. I think it's because it takes a moment to setState so on my first render the contextData array is empty, and after the state is set then the component could render.
Do you have any idea how to work around this or am I missing something? Should I use a different type of query? I'm querying all photos so I don't need to set any variables.
EDIT: I've found a solution for this kinda, basically I check if the data exits and I render my component conditionally.
return testData.length === 0 ? (
<div className="hidden">
<h2>Hey from test</h2>
<p>Nothing to render</p>
</div>
) : (
<div>
<h2>Hey from test</h2>
{testData.allStrapiCategory.allCategories.map((item) => (
<p>{item.name}</p>
))}
</div>
);
However, I find this hacky, and kinda repetitive as I'd have to use this in every component that I use that data at. So I'm still looking for other solutions.

Passing this [page queried] data to root provider doesn't make a sense [neither in gatsby nor in apollo] - data duplication, not required in all pages/etc.
... this data is fetched at build time then no need to check length/loading/etc
... you can render provider in page component to pass data to child components using context (without props drilling).

Related

Synchronising Event Handler for Search in React

I have been learning js and then React.js over the last few weeks, following tutorials on Codecademy and then Educative.io (to learn with the new hooks, rather than the class-based approach). In an attempt to apply what I have learned I have been messing around creating a number of common website features as React components on a hello-world project.
Most recently I have been trying to make a search component, which uses the Spotify API to search for a track, but have been running into synchronisation issues which I can't quite figure out how to solve using the js synchronisation tools that I know of. I come from a Java background so am more familiar with mutexes/semaphores/reader-writer locks/monitors so it may be that I am missing something obvious. I have been basing the code on this blog post.
In my implementation, I currently have a SongSearch component, which is passed its initial search text as a property, as well as a callback function which is called when the input value is changed. It also contains searchText as state, which is used to change the value of the input.
import * as React from 'react';
interface Props {
initialSearchText: string,
onSearchTextUpdated: (newSearchText: string) => void;
}
export const SongSearch = (props: Props) => {
const [searchText, setSearchText] = React.useState(props.initialSearchText);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSearchText = e.target.value;
setSearchText(newSearchText);
props.onSearchTextUpdated(newSearchText);
}
return <input value={searchText} onChange={onChange}/>;
};
The results are currently just displayed a list in the SearchResults component, the values of which are passed as an array of songs.
import * as React from 'react';
import { SongInfo } from './index';
interface Props {
songs: SongInfo[]
}
export const SearchResults = (props: Props) => {
return (
<ul>
{props.songs.map((song) => {
return <li key={song.uri}>{song.name}</li>
})}
</ul>
);
}
In the App component, I pass a callback function which sets the state attribute searchText to the new value. This then triggers the effect which calls updateSongs(). If we have an auth token, and the search text isn't empty we return the results of the API call, otherwise we return an empty list of songs. The result is used to update the tracks attribute of the state using setTracks().
I have cutdown the code in App.tsx to only the relevant parts:
import SpotifyWebApi from 'spotify-web-api-js';
import React from "react";
// ... (removed irrelevant code)
async function updateSongs(searchText: string): Promise<SongInfo[]>{
if (spotify.getAccessToken()) {
if (searchText === '') {
console.log('Empty search text.');
return [];
} else {
// if access token has been set
const res = await spotify.searchTracks(searchText, {limit: 10});
const tracks = res.tracks.items.map((trackInfo) => {
return {name: trackInfo.name, uri: trackInfo.uri};
});
console.log(tracks);
return tracks;
}
} else {
console.log('Not sending as access token has not yet');
return [];
}
}
function App() {
// ... (removed irrelevant code)
const initialSearchText = 'Search...';
const [tracks, setTracks] = React.useState([] as SongInfo[]);
const [searchText, setSearchText] = React.useState(initialSearchText);
React.useEffect(() => {
updateSongs(searchText)
.then((newSongs) => setTracks(newSongs))
}, [searchText]);
const content = <SearchResults songs={tracks}/>;
return (
<ThemeProvider theme={theme}>
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Root config={mui_config}>
<Header
renderMenuIcon={(open: boolean) => (open ? <ChevronLeft /> : <MenuRounded />)}
>
<SongSearch initialSearchText={initialSearchText} onSearchTextUpdated={(newSearchText) => {
console.log(`New Search Text: ${newSearchText}`)
setSearchText(newSearchText);
}}/>
</Header>
<Nav
renderIcon={(collapsed: boolean)=>
collapsed ? <ChevronRight /> : <ChevronLeft />
}
classes={drawerStyles}
>
Nav
</Nav>
<StickyFooter contentBody={content} footerHeight={100} footer={footerContent}/>
</Root>
</div>
</ThemeProvider>
);
}
export default App;
The issue that I am having is that when I type in the name of a long song and then hold down backspace sometimes songs remain displayed in the list even when the search text is empty. From inspection of the console logs in the code I can see that the issue arises because the setTracks() is sometimes called out of order, in particular when deleting 'abcdef' quickly setTracks() the result of updateTracks('a') will be called after the result of updateTracks(''). This makes sense as '' does not require any network traffic, but I have spent hours trying to work out how I can synchronise this in javascript with no avail.
Any help on the matter would be greatly appreciated!
In your case the results are coming back differently because you send multiple events, and the ones that come first - fire a response and then you display it.
My solution would be to use a debounce function on the onChange event of the input field. So that the user will first finish typing and then it should start the search. Although there still might be some problems, if one search has started and the user started typing something else then the first one has finished and the second one has started and finished. In this you might find that cancelling a request helpful. Unfortunately you can't cancel a Promise, so you would have to read about RxJS.
Here's a working example using debounce
P.S.
You might find this conference talk helpful to understand how the event loop is working in JS.

Gatsby application crashes with error 'Cannot read property' even though property can be console logged

I am trying to use images from a headless CMS using Gatsby, but whenver I access my 'mainImage' I get an error stating TypeError: Cannot read property '<any value in the image here>' of null
I have the following query in my Gatsby blog.js page
export const query = graphql`
query BlogPageQuery {
posts: allSanityPost(
limit: 12
sort: { fields: [publishedAt], order: DESC }
) {
edges {
node {
id
publishedAt
mainImage {
alt
asset {
fluid(maxWidth: 1080) {
...GatsbySanityImageFluid
}
}
}
title
_rawExcerpt
slug {
current
}
}
}
}
}
`
The result of this is passed to a component that is responsible for generating a blog grid like so
{data ? (
<BlogPostPreviewGrid blogPosts={data} />
) : (
<p>Could not get blog data</p>
)
}
the BlogPostPreviewGrid is currently only used to pass data to my BlogPostPreview component like so
const BlogPostPreviewGrid = ({blogPosts}) => {
return (
<div>
{blogPosts.posts.edges.map (({ node }) => (
<BlogPostPreview blogPosts={node} key={node.id}/>
))}
</div>
)
}
My full BlogPostPreview.js component looks like this
import React from "react"
function previewFunction(props) {
return (
<div>
<h1>{props.blogPosts.title}</h1>
{console.log(props.blogPosts)}
{console.log(props.blogPosts.mainImage)}
</div>
)
}
const BlogPostPreview = props => {
return (
<>
{props ? (
previewFunction(props)
) : (
<p>Failed to get blog data in blogPreview component</p>
)
}
</>
)
}
export default BlogPostPreview
Using the console.log statments in BlogPostPreview.js I can see all my blogpostdata and my blogPosts.mainImage data. But if I try to call something in mainImage I get the error.
Example:
If I try to run
{console.log(props.blogPosts.mainImage.alt)}
See the attached screenshot to see the result of {console.log(props)}
Screensot of devtools showing results of console.log(props)
My goal is to be able to use the images in the gatsby Img component like so:
<Img fixed={props.mainImage.asset.fixed}
alt={props.mainImage.alt}
Calling blogPopsts.title works fine, so this runs without issues and prints the expected results
<h1>{props.blogPosts.title}</h1>
Thank you for all replies and comments!
Edit: I can see the results of console.logs targeting mainImage, but the app still crashes. I also see that the 'key' field is empty
This is usually a thing when there is not at least one instance of the data you are trying to access existing in the cms. A workaround we have used is to have placeholder/dummy data created in the cms. Another is to set default/fall back schema using Gatsby.js schema customization (see their docs for more info.)
Also make sure you clicked published in whatever headless cms you are using. For example I have had this issue when I forgot to click on 'publish' in the cms. So the content was in draft mode which Gatsby then would not be able to get data for.

React - show hide two elements without flickering on page load

Edit. I rewrote the code to be even more minimalist. The below code is a spike test of my issue.
Here is a video of the issue:
https://imgur.com/a/WI2wHMl
I have two components.
The first component is named TextEditor (and it is a text editor) but its content is irrelevant - the component could be anything. A simple div with text would be just as relevant.
The next component is named Workflows and is used to render a collection from IndexDB (using the Dexie.js library). I named this collection "workflows" and the state variable I store them in is named workflows_list_array
What I am trying to do is the following:
When the page loads, I want to check if any workflows have a specific ID . If they do, I store them in workflows_list_array and render them. I don't need help with this part.
However, if no workflows with the aforementioned criteria exist, I want to keep the component named Workflows hidden and render the component named TextEditor. If workflows do exist, I want the TextEditor hidden and to display Workflows
The problem is that even though I have it working, when workflows do exist (when workflows_list_array is populated) the TextEditor "flickers" briefly before being hidden and then the Workflows component is displayed.
I can tell this is an async issue but I can't tell how to fix it.
I posted code below and I tried to keep it to a minimum.
Test.js
import React, {useState, useEffect} from "react";
import db from "../services"
function Workflows(props){
return (
<div>
<ul>
{
props.workflows.map((val,index)=>{
return <li key={index}>{val.content}</li>
})
}
</ul>
</div>
)
}
function TextEditor(){
return (
<div> TextEditor </div>
)
}
function Test(props){
let [workflows_list_array, set_state_of_workflows_list_array] = useState([]);
let [client_id_number, set_client_id_number] = useState(5);
useEffect(() => { // get all workflows of the selected client per its ID
db.workflows.toArray((workflows_list)=>{ // iterate through workflows array
return workflows_list
}).then((workflows_list)=>{
workflows_list.forEach((val)=>{
if(client_id_number === val.client_id){
set_state_of_workflows_list_array((prev)=>{
return [...prev, val]
});
}
});
});
}, []);
return(
<div>
{workflows_list_array.length ? null : <TextEditor/> }
{workflows_list_array.length ? <Workflows workflows={workflows_list_array}/> : null}
</div>
)
}
export default Test
services.js
import Dexie from 'dexie';
import 'dexie-observable';
var workflowMagicUserDB = new Dexie("WorkflowMagicUserDB");
workflowMagicUserDB.version(1).stores({
user: "",
workflows: "++id,client_id,content,title",
clients: "++id,name",
calendar_events: "++id,start,end,title"
});
export default workflowMagicUserDB
Why don't you include a flag which indicates if you have already got data from IndexDB, something like:
function Test(props){
const [loading, setLoading] = React.useState(true);
let [workflows_list_array, set_state_of_workflows_list_array] = useState([]);
let [client_id_number, set_client_id_number] = useState(5);
useEffect(() => {
db.workflows.toArray((workflows_list)=>{
}).then((workflows_list)=>{
}).finally(() => setLoading(false)); //when process finishes, it will update the state, at that moment it will render TextEditor or Workflows
}, []);
if(loading) return <LoadingIndicator/>; // or something else which indicates the app is fetching or processing data
return(
<div>
{workflows_list_array.length ? null : <TextEditor/> }
{workflows_list_array.length ? <Workflows workflows={workflows_list_array}/> : null}
</div>
)
}
export default Test
When the process finishes, finally will be executed and set loading state to false, after that, your app will render TextEditor or Workflows

Updating post count the reactive way

I am new to react. I have created a news component that consumes a json url then spits out some news articles. In the client side UI if the clients changes the json url it will update without refreshing the page using this code:
componentDidUpdate(prevProps) {
if (prevProps.jsonUrl !== this.props.jsonUrl) {
this.getPosts();
}
}
However I also need the the news feed to update reactively if the postCount: this.props.postCount is changed in the client side UI. The post count is used in the render method below to choose how many posts to display.
posts
.slice(0, postCount)
.map(post => {
// Variables to use
let { id, name, summary, url, imgUrl} = post;
// Stripping html tags from summary
//let strippedSummary = summary.replace(/(<([^>]+)>)/ig,"");
// What we actually render
return (
<div key={id} className={ styles.post}>
<p>{name}</p>
{/* <p>{summary}</p> */}
<a href={url}>{url}</a>
<img className={ styles.postImage} src={imgUrl} />
</div>
);
})
Any help is much appreciated! - I was thinking something like this inside componentDidUpdate:
if (prevProps.postCount !== this.props.postCount) {
this.setState( this.state.postCount; );
}
EDIT:
I am now using the postCount from the props instead of a state and it updates instantly! :D
// Grabbing objects to use from state
const { posts, isLoading } = this.state;
const { postCount } = this.props;
The components are going to react automatically to the changes in their props, so there's no need to transfer any props to a state. In this case, if postCount is a prop, when it changes it should affect the piece of code that you shared to render the component. However, I don't know if posts is part of the state, in your case it should be and your method getPosts should setState with the new posts.

React.js "global" component that can be created multiple times

I can't get my head wrapped around this.
The problem: let's say there's an app and there can be some sort of notifications/dialogs/etc that i want to create from my code.
I can have "global" component and manage it, but it would limit me to only one notification at a time, this will not fit.
render() {
<App>
// Some components...
<Notification />
</App>
}
Or i can manage multiple notifications by the component Notification itself. But state management will not be clear.
The other problem if i have some sort of user confirmation from that component (if it's a confirmation dialog instead of simple notification) this will not be very convinient to handle with this solution.
The other solution is to render a component manually. Something like:
notify(props) {
const wrapper = document.body.appendChild(document.createElement('div'))
const component = ReactDOM.render(React.createElement(Notification, props), wrapper)
//...
// return Promise or component itself
}
So i would call as:
notify({message: '...'})
.then(...)
or:
notify({message: '...', onConfirm: ...})
This solution seems hacky, i would like to let React handle rendering, and i have an additional needless div. Also, if React API changes, my code breaks.
What is the best practice for this scenario? Maybe i'm missing something completely different?
You could use React Context for this.
You create a React context at a high level in your application and then associate a values to it. This should allow components to create / interact with notifications.
export const NotificationContext = React.createContext({
notifications: [],
createNotification: () => {}
});
class App extends Component {
constructor() {
super();
this.state = {
notifications: []
};
this.createNotification = this.createNotification.bind(this);
}
createNotification(body) {
this.setState(prevState => ({
notifications: [body, ...prevState.notifications]
}));
}
render() {
const { notifications } = this.state;
const contextValue = {
notifications,
createNotification: this.createNotification
};
return (
<NotificationContext.Provider value={contextValue}>
<NotificationButton />
{notifications.map(notification => (
<Notification body={notification} />
))}
</NotificationContext.Provider>
);
}
}
The notifications are stored in an array to allow multiple at a time. Currently, this implementation will never delete them but this functionality can be added.
To create a notification, you will use the corresponding context consumer from within the App. I have added a simple implementation here for demonstration purposes.
import { NotificationContext } from "./App.jsx";
const NotificationButton = () => (
<NotificationContext.Consumer>
{({ notifications, createNotification }) => (
<button onClick={() => createNotification(notifications.length)}>
Add Notification
</button>
)}
</NotificationContext.Consumer>
);
You can view the working example here.

Categories