Am I using React wrong? - javascript

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.

Related

Data Fetching Using useEffect() And useCallback In React

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();
}, []);

How to display "no records found" after running a search in React

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.

How can I pass props from one component to another withour Redux

I have this kind of jsx and I want to pass number value from SendNumberPage to CheckNumberPage.
App.js
<EditNumberPage/>
<br/>
<SendNumberPage/>
<br/>
<CheckNumberPage/>
SendNumberPage.js
function onChangeHandler(event) {
setState({
...state,
number: event.target.value
})
}
I tried using React.createContext but it didn't work for me. Please Help
sendNumberPage.js
const [state, setState] = useState(
{
number: '+99979787'
}
)
const NumberContext = React.createContext()
return (
<NumberContext.Provider value={state.number}>
<div>
....
....
</div>
</NumberContext.Provider>
)
checkNumberPage.js
const CheckNumberPage = () => {
const value = useContext(NumberContext)
console.log(value)
return (
.......
)
}
Console says Attempted import error: 'NumberContext' is not exported from './SendNumberPage'.
Depending on how complex your app is you may want to do this in different ways.
Using react context api is a good way to do it, and it is scalable and suitable for all app sizes.
You should check out the react tutorials for that.
If your app is very small (1 layer) and you just want a 'quick fix' you could pass a change listener callback to one component and update the props in the other component.
<EditNumberPage/>
<br/>
<SendNumberPage onChange={(n) => {setNumber(n)} />
<br/>
<CheckNumberPage number={number}/>

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

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).

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.

Categories