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.
Related
Issue
I'm looking for the most optimal way to fetch data using useEffect() when the fetch function is used in more than one place.
Situation
Currently, I have a parent component (ItemContainer) and a child component (SearchBar). ItemContainer should fetch the all the possible list of items using getItemList() functions. I'm executing this function within the useEffect() during the first render, and also passing it down to SearchBar component, so that when a user submits a search term, it will update itemList state by triggering getItemList() in ItemContainer.
This actually works just as I expected. However, my issue is that
I'm not really sure whether it is okay to define getItemList() outside the useEffect() in this kind of situation. From what I've been reading (blog posts, react official docs) it is generally recommended that data fetching function should be defined inside the useEffect(), although there could be some edge cases. I'm wondering if my case applies as this edge cases.
Is it okay to leave the dependency array empty in useCallback? I tried filling it out using searchTerm, itemList, but none of them worked - and I'm quite confused why this is so.
I feel bad that I don't fully understand the code that I wrote. I would appreciate if any of you could enlighten me with what I'm missing here...
ItemContainer
const ItemContainer = () => {
const [itemList, setItemList] = useState([]);
const getItemList = useCallback( async (searchTerm) => {
const itemListRes = await Api.getItems(searchTerm);
setItemList(itemListRes)
}, []);
useEffect(() => {
getItemList();
}, [getItemList]);
return (
<main>
<SearchBar search={getItemList} />
<ItemList itemList={itemList} />
</main>
)
}
SearchBar
const SearchBar = ({ search }) => {
const [searchTerm, setSearchTerm] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
search(searchTerm);
setSearchTerm('');
}
const handleChange = (e) => {
setSearchTerm(e.target.value)
}
return (
<form onSubmit={handleSubmit}>
<input
placeholder='Enter search term...'
value={searchTerm}
onChange={handleChange}
/>
<button>Search</button>
</form>
)
}
Here are my answers.
Yes, it is okay. What's inside useCallback is "frozen" respect to
the many ItemConteiner function calls that may happen. Since the
useCallback content accesses only setItemList, which is also a
frozen handler, there'll be no problems.
That's also correct, because an empty array means "dependent to
nothing". In other words, the callback is created once and keeps
frozen for all the life of the ItemContainer.
Instead, this is something weird:
useEffect(() => {
getItemList();
}, [getItemList]);
It works, but it has a very little sense. The getItemList is created once only, so why make an useEffect depending to something never changes?
Make it simpler, by running once only:
useEffect(() => {
getItemList();
}, []);
React: am I doing it wrong?
So I’ve been working with React for a while, and I’ve been able to create some really cool projects by utilizing what React has to offer; Hooks, props, etc. The thing is. My workflow always comes to a stop and I end up having a bad case of spaghetti-code when I try to pass variables and state between local and global functions. 9/10 I end up getting stuck and disobeying the React Hooks Rules, and have hack my way out of it with a very vanilla JS way of doing things. And then I think to myself: “What a wonderf… No, I mean: Why am I using React if I end up writing vanilla JS when I try to do something that is a bit more advanced than rendering components on a page?”. Is my approach all wrong?
Here's an example: I have a webpage which fetches to an API written in Express, which in turn returns data from a MongoDB database. I use a custom hook to fetch with an async function, and then I display everything on a page. I have a functional component that renders out everything. I also send some query-data with the API fetch, which in this example is a string representation of numbers, which in turn sets the limit of how many elements are gathered from the database. And on the useEffect hook – which is inside the custom hook I mentioned earlier – I have the number of elements to display as a dependency, so that I fetch the API every time that value changes. That value in turn, is chosen by a slider between 1-1000. Every time I fetch, the component renders again and everything flashes. This is because the data from the DB, as well as my h1, slider, and p-tags, are all in the same component. I want to avoid that, so my initial thought is to extract everything BUT the data from the DB, to a different component and render it separately. And this is where it goes wrong. The slidervalue which sets state, which in turn the custom hook uses to send as a query parameter to the API, they do not have any connection to each other anymore. Am I using React all wrong? Is this where the context API would be smart to use?
I basically want to share state between to different functional components, and render them separately on a webpage.
This is my frontend code:
import React, { useEffect, useState } from "react";
function useLoading(loadingFunction, sliderValue) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [data, setData] = useState([]);
async function load() {
try {
setLoading(true);
setData(await loadingFunction());
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, [sliderValue]);
return { loading, error, data };
}
async function fetchJSON(url, sliderValue) {
const res = await fetch(url + `?numberOfMovies=${sliderValue}`);
if (!res.ok) {
throw new Error(`${res.status}: ${res.statusText}`);
}
return await res.json();
}
function randomNumber() {
return Math.floor(Math.random() * 20000000000);
}
function LoadingPage() {
return (
<>
<div className="loading one" />
<div className="loading two" />
<div className="loading three" />
<div className="loading four" />
</>
);
}
function MovieCard({ movie: { title, plot, year, poster } }) {
return (
<div className={"movie-card"}>
<h3>
{title} ({year})
</h3>
<p>{plot}</p>
{poster && <img width={100} src={poster} alt="Poster" />}
</div>
);
}
function ListMovies() {
const [sliderValue, setSliderValue] = useState("300");
const { loading, error, data } = useLoading(
async () => fetchJSON("/api/movies", sliderValue),
sliderValue
);
if (loading) {
return <LoadingPage />;
}
if (error) {
return (
<div>
<h1>Error</h1>
<div>{error.toString()}</div>
</div>
);
}
function handleSliderChange(e) {
let value = (document.getElementById("slider").value = e.target.value);
document.getElementById("slider-value").innerHTML =
value <= 1 ? `${value} movie` : `${value} movies`;
setSliderValue(value);
}
return (
<div className={"movies-container"}>
<h1>Movies</h1>
<p>Sorted by highest rated on Metacritic. All movies are from Ukraine.</p>
<input
onChange={handleSliderChange}
type="range"
min="1"
max="1000"
className="slider"
id="slider"
/>
<p id="slider-value" />
<div>
{data.map((movie) => (
<MovieCard key={randomNumber()} movie={movie} />
))}
</div>
</div>
);
}
export function MainPage() {
return (
<div>
<ListMovies />
</div>
);
}
It might be enough to "lift" the state to a common ancestor. State management in React is a surprisingly complex topic and worth reading up on standard approaches. Lifting state is one of them, because components don't "usually" talk to each other "horizontally". Props flow down. There are other ways to manage this such as Context or Redux, or even "non" React approaches such as pub/sub.
The good news is that having experienced the pain points first hand, you'll appreciate some of the patterns for solving the problems.
In my opinion I'm not sure there is a "wrong" way to do things, as long as it works. But there are definitely approaches that make life hard and others that make life easier.
If you could whittle down your issue to a very specific question, without so much explanation, you're likely to get better help.
I have been trying to add "no records found" message after running a search for worker names. But I have not been successful. I either get 20 "no records found" messages or none at all. I am not sure what I am doing wrong, but I have been trying for last 4 hours various methods and work arounds.
I know that this should be simple to implement, but it has been difficult.
Here is a link to my code on codesandbox: https://codesandbox.io/s/fe-hatc-ass-search-n62kw?file=/src/App.js
Any insights would be helpful....
Things I tried were, if else statements, logical operators... etc...
In my opinion the first thing you need to think about is what data do you need and when do you need it. To display no results like you want you are going to need the workers name in the component that is doing the filtering. So you would need it in the orders component. I would merge the worker data with the order data and then you can just filter and manipulate the data after that. That would also stop you from making an api request every time someone changes the input and all you need to do is filter the already fetched data. Then you can check the array length and if it is greater than 0 you can display results else display a no results statement.
So something like the following:
Orders component
import React, { useEffect, useState } from "react";
import "./Orders.css";
import Order from "./Worker";
import axios from "axios";
const Orders = () => {
const [orders, setOrders] = useState([]);
const [results, setResults] = useState([]);
const [searchedWorker, setSearchedWorker] = useState("");
const getOrders = async () => {
const workOrders = await axios.get(
"https://api.hatchways.io/assessment/work_orders"
);
const mappedOrders = await Promise.all(workOrders.data.orders.map(async order => {
const worker = await axios.get(
`https://api.hatchways.io/assessment/workers/${order.workerId}`
);
return {...order, worker: worker.data.worker}
}))
setOrders(mappedOrders);
};
useEffect(() => {
getOrders();
}, []);
useEffect(() => {
const filtered = orders.filter(order => order.worker.name.toLowerCase().includes(searchedWorker));
setResults(filtered)
}, [searchedWorker, orders])
return (
<div>
<h1>Orders</h1>
<input
type="text"
name="workerName"
id="workerName"
placeholder="Filter by workername..."
value={searchedWorker} //property specifies the value associated with the input
onChange={(e) => setSearchedWorker(e.target.value.toLowerCase())}
//onChange captures the entered values and stores it inside our state hooks
//then we pass the searched values as props into the component
/>
<p>Results: {results.length}</p>
{results.length > 0 ? results.map((order) => (
<Order key={order.id} lemon={order} />
)) : <p>No results found</p> }
</div>
);
};
//(if this part is true) && (this part will execute)
//is short for: if(condition){(this part will execute)}
export default Orders;
Then you can simplify your single order component
import React from "react";
const Order = ({ lemon }) => {
return (
<div>
<div className="order">
<p>Work order {lemon.id}</p>
<p>{lemon.description}</p>
<img src={`${lemon.worker.image}`} alt="worker" />
<p>{lemon.worker.name}</p>
<p>{lemon.worker.company}</p>
<p>{lemon.worker.email}</p>
<p>{new Date(lemon.deadline).toLocaleString()}</p>
</div>
</
div>
);
};
export default Order;
Looking at your code, the problem is because you're doing the filtering in each individual <Order> component. The filtering should be done in the parent Orders component and you should only render an <Order> component if a match is found.
Currently, your <Order> component is rendering, even if there's no match.
You could add an state in the Orders.js to count how many items are being presented. However, since each Worker depends on an api call, you would need to have the response (getWorker, in Workers.js) wait for the response in order to make the count. Every time the input value changes, you should reset the counter to 0.
https://codesandbox.io/s/fe-hatc-ass-search-forked-elyjz?file=/src/Worker.js:267-276
Also, as a comment, it is safer to put the functions that are run in useEffect, inside the useEffect, this way it is easier to see if you are missing a dependency.
I am building this PWA with ReactJS.
As the title says, I just want to play a sound whenever a new order is placed.
I am using the Context API in order to manage the states.
This is my OrderContext.js where I fetch the orders from the API using React Query.
import React, { createContext } from "react";
import { useQuery } from "react-query";
import apiClient from "../EventService";
export const OrderContext = createContext();
export const OrderProvider = (props) => {
const fetchOrders = async () => {
const { data } = await apiClient.getEvents();
return data;
};
let { data: orders, status } = useQuery("orders", fetchOrders, {
refetchInterval: 15000,
refetchIntervalInBackground: true,
notifyOnStatusChange: false,
});
return (
<OrderContext.Provider value={[orders, status]}>
{props.children}
</OrderContext.Provider>
);
};
Then somewhere in my OrdersList.js component, I just use the state and map through it:
{orders.map((order) => (
<Order key={order.id} order={order} />
))}
And lastly, in my Order.js component is where I check if there is a new order or not:
I am using this function to check if the order has been placed 15 or less minutes ago.
const isNewOrder = (orderCreatedDate) => {
if (moment().diff(moment(orderCreatedDate)) / 900000 <= 1) {
return true;
} else {
return false;
}
};
and then in the in the JSX of the component I simply do this to display a badge on the new orders:
{isNewOrder(order.date_created) && (
<Badge style={{ padding: 3 }} content="Nuevo" />
)}
I am having troubles adding a sound effect to these new orders, I tried using Howler but couldn't make it work properly.
If someone could help me or maybe show me a way to achieve this I would really appreciate. I can provide the full components if needed, let me know. Thanks in advance.
Have a nice day!
I think your best approach is to add a prop to your Order component and check if you are on the order list page or on the order page. If on one single order then you call sound.play, otherwise you do not.
Also, you should use React Howler if you're not already. Regular Howler does not play well (no pun intended) with React.
Finally, you should add a componentWillUnmount function to stop the sound from playing (just call sound.stop() inside componentWillUnmount).
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