I'm send some settings data in useEffect:
const [settingsData, setSettingsData] = useState([]);
const [settingsLoading, setSettingsLoading] = useState(true);
useEffect(() => {
if (isAuth) {
apiClient
.get("api/v1/page-data/settings")
.then((response) => {
setSettingsData(response.data);
setSettingsLoading(false);
})
.catch((error) => console.error(error));
}
}, []);
And create useStates for two keys inputs with data from backend:
const [publicKey, setPublicKey] = useState(
settingsData?.data?.user_data?.public_key
);
const [secretKey, setSecretKey] = useState(
settingsData?.data?.user_data?.secret_key
);
There is an inputs:
<input
value={publicKey}
onChange={(e) => setPublicKey(e.target.value)}
placeholder="PublicKey"
/>
<input
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
placeholder="SecretKey"
/>
When page is loaded i'm just see empty inputs, looks like useState assign my backend property when it's undefined. How can i assign backend data to useState if this hook initialize initial value before my settings data is loaded from backend?
Your key related initial states are set to undefined because the data is not available when they are created. There are a few ways to solve that, here are a couple:
You can set those key related values at the same time you set your data object instead of trying to derive them
const [settingsData, setSettingsData] = useState([]);
const [settingsLoading, setSettingsLoading] = useState(true);
const [publicKey, setPublicKey] = useState('');
const [secretKey, setSecretKey] = useState('');
useEffect(() => {
if (isAuth) {
apiClient
.get("api/v1/page-data/settings")
.then(({ data }) => {
setSettingsData(data);
setSettingsLoading(false);
setPublicKey(data.user_data.public_key);
setSecretKey(data.user_data.secret_key);
})
.catch((error) => console.error(error));
}
}, []);
A less optimal option would be to use another useEffect to update your key telated states
const [settingsData, setSettingsData] = useState([]);
const [publicKey, setPublicKey] = useState('');
const [secretKey, setSecretKey] = useState('');
useEffect(() => {
if (settingsData) {
setPublicKey(settingsData.user_data.public_key);
setSecretKey(settingsData.user_data.secret_key);
}
}, [settingsData]);
a third option is to make only the initial state of the inputs derived from the returned data and use short circuiting to manage the value
const [settingsData, setSettingsData] = useState([]);
const [settingsLoading, setSettingsLoading] = useState(true);
const [publicKey, setPublicKey] = useState('');
const [secretKey, setSecretKey] = useState('');
const publicK = settingsData?.data?.user_data?.public_key;
const secretK = settingsData?.data?.user_data?.secret_key;
useEffect(() => {
if (isAuth) {
apiClient
.get("api/v1/page-data/settings")
.then(({ data }) => {
setSettingsData(data);
setSettingsLoading(false);
setPublicKey(data.user_data.public_key);
setSecretKey(data.user_data.secret_key);
})
.catch((error) => console.error(error));
}
}, []);
// inputs will start with the values from const but once you write anything
// in the input it will change to the managed value in state
<input
value={publicKey || publicK}
onChange={(e) => setPublicKey(e.target.value)}
placeholder="PublicKey"
/>
<input
value={secretKey || secretK}
onChange={(e) => setSecretKey(e.target.value)}
placeholder="SecretKey"
/>
Related
Whenever I dispatch a search action using context and useReducer for an object in an array stored in local storage, it returns the object, but when I delete the search query from the input box, the list is not returned and the page is blank, can anyone help please?
This is my context:
const NotesContext = createContext(null);
const NotesDispatchContext = createContext(null);
const getStoredNotes = (initialNotes = InitialNotes) => {
return JSON.parse(localStorage.getItem("storedNotes")) || initialNotes;
};
export const NotesProvider = ({ children }) => {
const [NOTES, dispatch] = useReducer(NotesReducer, getStoredNotes());
useEffect(() => {
localStorage.setItem("storedNotes", JSON.stringify(NOTES));
}, [NOTES]);
return (
<NotesContext.Provider value={NOTES}>
<NotesDispatchContext.Provider value={dispatch}>
{children}
</NotesDispatchContext.Provider>
</NotesContext.Provider>
);
};
export const useNotesContext = () => {
return useContext(NotesContext);
};
export const useNotesDispatchContext = () => {
return useContext(NotesDispatchContext);
};
const App = () => {
const [query, setQuery] = useState("");
const dispatch = useNotesDispatchContext();
useEffect(() => {
if (query.length !== 0) {
dispatch({
type: "searchNotes",
query: query,
});
}
}, [query]);
return (
<div className="container">
<header>
<Title title={"Notes"} className={"app_title"} />
<form className="search_container">
<span class="material-symbols-outlined">search</span>
<input
type="search"
placeholder="search notes"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
</header>
This is my reducer function
case "searchNotes": {
[...NOTES].filter((note) =>
note.title.toLowerCase().includes(action.query)
);
}
The function seems to actually remove the all data from the local storage instead of filtering based on the query string.
Issue
When you dispatch searchNotes you are changing NOTES and the blow useEffect runs. So if the filter resulted to an empty array, there would be nothing in localStorage.
useEffect(() => {
localStorage.setItem("storedNotes", JSON.stringify(NOTES));
}, [NOTES]);
Solution
What you can do is to remove that useEffect in App that has query as dependency and dispatching searchNotes. And filter directly while rendering, something like this:
{
NOTES.filter((note) => note.title.toLowerCase().includes(query)).map((note, index) => (
<div key={index}>{note.title}</div>
))
}
And at this point you can remove searchNotes case from your reducer.
I'm able to fetch the data but not sure how to map nested items
data looks like that
{"id":3,
"brands":[{"id":6,"products":[],
"title":"Lenovo",
"image":"/img.png",
"timestamp":"2022-02-07T15:11:36.059719+05:30",
"updated":"2022-02-07T15:11:36.059719+05:30",
"category":3}],
"title":"Laptop",
"image":"/img.png",
"timestamp":"2022-01-30T23:18:14.124583+05:30",
"updated":"2022-01-30T23:18:14.124583+05:30"}
I'm using following method to do this.
const CategoriesToBrands = ({ match }) => {
const [hasError, setErrors] = useState(false);
const [items, setItems] = useState({});
const { id } = useParams();
async function fetchData() {
const data = await fetch("/api/v1/categories/nested/" + id);
data.json()
.then(data => setItems(data))
.catch(data => setErrors(data));
}
useEffect(() => {
fetchData()
}, [])
return (
<div>
<div>{JSON.stringify(items)}</div>
<hr />
{items.title}
<hr />
<div>Has error: {JSON.stringify(hasError)}</div>
</div>
);
};
export default CategoriesToBrands;
I also tried multiple method suggested on stack overflow to map the items but no one seems to be working . or they are using another method . there can be multiple number of items in brands [] and i want to display each one of them for now there is one.. So what would be the syntax or method to map them properly.
You have an brands array inside your main object fetched by async request. It means you should to get brands by items.brands. Something like that:
const CategoriesToBrands = ({ match }) => {
const [hasError, setErrors] = useState(false);
const [items, setItems] = useState({});
const { id } = useParams();
async function fetchData() {
const data = await fetch("/api/v1/categories/nested/" + id);
data.json()
.then(data => setItems(data))
.catch(data => setErrors(data));
}
useEffect(() => {
fetchData()
}, [])
return (
<div>
<div>{JSON.stringify(items)}</div>
<hr />
{items.title}
<hr />
{items.brands ? items.brands.map(brand => (
<div key={brand.id}>brand id: {brand.id}</div>
)) : null}
<div>Has error: {JSON.stringify(hasError)}</div>
</div>
);
};
export default CategoriesToBrands;
The good idea is also changing the name of your const [items, setItems] = useState({});. In your case you are fetching one item, not multiple (I assume this fact by your id variable). So the name like item, setItem sounds better.
So I'm using useEffect hook to fetch my data from database and after I get that data I want to set it as useState for title and postBody, but it doesn't work, because useEffect hook runs "last", how can I fix it?
Code:
const [cPost, setCPost] = useState([]);
const postId = id.match.params.id;
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
console.log("SAS");
})
}, []);
const [title, setTitle] = useState(cPost.title);
const [postBody, setPostBody] = useState(cPost.postBody);
As a temporary and quick solution, you can use such workaround:
const [cPost, setCPost] = useState();
const [title, setTitle] = useState();
const [postBody, setPostBody] = useState();
const postId = id.match.params.id;
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(post => {
setCPost(post.data);
console.log("SAS");
})
}, []);
useEffect(() => {
if(cPost) {
setTitle(cPost.title);
setPostBody(cPost.postBody);
}
}, [cPost]);
Or the second option:
const [cPost, setCPost] = useState();
const [title, setTitle] = useState();
const [postBody, setPostBody] = useState();
const postId = id.match.params.id;
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(post => {
setCPost(post.data);
setTitle(post.title);
setPostBody(post.postBody);
console.log("SAS");
})
}, []);
But in the future I would recommend doing side effects like API requests and others using special libraries or create hook for making API requests.
For example redux-saga or redux-thunk.
And use a state manager like redux or mobx.
P.S. and consider whether you need to store the title and body separately in the component state. I have a strong suspicion that you have no need for it.
I'm trying to implement a refresh button but can't get it done.
This is how my code looks like:
// ParentComponent.js
const ParentComponent = () => {
const { loading, error, data } = useItems();
return (
<ChildComponent items={data} />
);
... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items }) => {
return (
// Logic that renders the items in <li>s
<button onClick={() => console.log('Clicking this button should refresh parent component')}
)
};
// services/useItems.js
const useItems = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios
.get(API_URL + '/counter')
.then((response) => {
setItems(response.data);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(error.message);
});
}, []);
return { loading, error, data: counters };
}
I've tried several ways but none did the work. any helps would be truly appreciated :)
I don't think useEffect is the right mechanism here. Since it's an imperative call, nothing reactive about it, useState does the job just fine:
// ParentComponent.js
const ParentComponent = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const refresh = () => {
axios.get(API_URL + '/counter').then((response) => {
setItems(response.data);
setLoading(false);
}).catch((error) => {
setLoading(false);
setError(error.message);
});
};
useEffect(refresh, []);
return (
<ChildComponent items={items} refresh={refresh} />
);
// ... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items, refresh }) => {
return (
// Logic that renders the items in <li>s
<button onClick={refresh}>
Refresh
</button>
)
};
A very simple trick is to increase an integer state, let's just call it version, which would trigger a re-render of <ParentComponent /> and if useEffect depends on version, it'll re-execute the callback, so you get the "refresh" effect.
// ParentComponent.js
const ParentComponent = () => {
const [version, setVersion] = useState(0)
// when called, add 1 to "version"
const refresh = useCallback(() => {
setVersion(s => s + 1)
}, [])
const { loading, error, data } = useItems(version);
return (
<ChildComponent items={data} refresh={refresh} />
);
};
// ChildComponent.js
const ChildComponent = ({ items, refresh }) => {
return (
// Logic that renders the items in <li>s
<button onClick={refresh} />
)
};
// services/useItems.js
const useItems = (version) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios
.get(API_URL + '/counter')
.then((response) => {
setItems(response.data);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(error.message);
});
}, [version]); // <-- depend on "version"
return { loading, error, data: counters };
}
There are couple fo small parts where you need to make changes to resolve issue.
You need to create a communication for refresh
Create a function to process any processing for refresh.
Pass this as a prop to child component
In child component, call it on necessary event, in this case click
Now since you are using hooks, you need to get it invoked.
You can add a function refreshData in your useItem hook and expose it
Call this function on click of button.
You will also have to add a flag in hooks and update useEffect to be triggered on its change
This function is necessary as setItems is only available inside hook.
Following is a working sample:
const { useState, useEffect } = React;
// ParentComponent.js
const ParentComponent = () => {
const { loading, error, data, refreshData } = useItems();
const refreshFn = () => {
refreshData()
}
return (
<ChildComponent
items={data}
onClick={refreshFn}/>
);
// ... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items, onClick }) => {
const onClickFn = () => {
console.log('Clicking this button should refresh parent component')
if(!!onClick) {
onClick();
}
}
return (
// Logic that renders the items in <li>s
<div>
<button
onClick={ () => onClickFn() }
>Refresh</button>
<ul>
{
items.map((item) => <li key={item}>{item}</li>)
}
</ul>
</div>
)
};
// services/useItems.js
const useItems = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [refresh, setRefresh] = useState(false)
useEffect(() => {
if (refresh) {
setItems(Array.from({ length: 5 }, () => Math.random()));
setRefresh(false)
}
}, [ refresh ]);
return {
loading,
error,
data: items,
refreshData: () => setRefresh(true)
};
}
ReactDOM.render(<ParentComponent/>, document.querySelector('.content'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='content'></div>
As correctly commented by hackape, we need to add a check for refresh and fetch data only if its true
I am making dummy app to test server side API.
First request returns nested JSON object with Product names and number of variants that it has. From there I extract Product name so I can send second request to fetch list of variants with product images, sizes etc.
Sometimes it will load and display variants from only one product but most of the times it will work correctly and load all variants from both dummy products.
Is there a better way of doing this to ensure it works consistently good. Also I would like to know if there is a better overall approach to write something like this.
Here is the code:
import React, { useEffect, useState } from "react";
import axios from "axios";
import ShirtList from "../components/ShirtList";
const recipeId = "15f09b5f-7a5c-458e-9c41-f09d6485940e";
const HomePage = props => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
axios
.get(
`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
)
.then(response => {
let shirtList = [];
const itemsLength = response.data.Products.length;
response.data.Products.forEach((element, index) => {
axios
.get(
`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${element.ProductName}`
)
.then(response => {
shirtList.push(response.data.Variants);
if (index === itemsLength - 1) {
setLoaded(shirtList);
}
});
});
});
}, []);
const ListItems = props => {
if (props.loaded) {
return loaded.map(item => <ShirtList items={item} />);
} else {
return null;
}
};
return (
<div>
<ListItems loaded={loaded} />
</div>
);
};
export default HomePage;
You are setting the loaded shirts after each iteration so you will only get the last resolved promise data, instead fetch all the data and then update the state.
Also, separate your state, one for the loading state and one for the data.
Option 1 using async/await
const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'
const fetchProducts = async () => {
const { data } = await axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
return data.Products
}
const fetchShirts = async productName => {
const { data } = await axios.get(
`${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
)
return data.Variants
}
const HomePage = props => {
const [isLoading, setIsLoading] = useState(false)
const [shirtList, setShirtList] = useState([])
useEffect(() => {
setIsLoading(true)
const fetchProductShirts = async () => {
const products = await fetchProducts()
const shirts = await Promise.all(
products.map(({ productName }) => fetchShirts(productName)),
)
setShirtList(shirts)
setIsLoading(false)
}
fetchProductShirts().catch(console.log)
}, [])
}
Option 2 using raw promises
const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'
const fetchProducts = () =>
axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
.then(({ data }) => data.Products)
const fetchShirts = productName =>
axios
.get(
`${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
)
.then(({ data }) => data.Variants)
const HomePage = props => {
const [isLoading, setIsLoading] = useState(false)
const [shirtList, setShirtList] = useState([])
useEffect(() => {
setIsLoading(true)
fetchProducts
.then(products) =>
Promise.all(products.map(({ productName }) => fetchShirts(productName))),
)
.then(setShirtList)
.catch(console.log)
.finally(() => setIsLoading(false)
}, [])
}
Now you have isLoading state for the loading state and shirtList for the data, you can render based on that like this
return (
<div>
{isLoading ? (
<span>loading...</span>
) : (
// always set a unique key when rendering a list.
// also rethink the prop names
shirtList.map(shirt => <ShirtList key={shirt.id} items={shirt} />)
)}
</div>
)
Refferences
Promise.all
Promise.prototype.finally
React key prop
The following should pass a flat array of all variants (for all products ) into setLoaded. I think this is what you want.
Once all the products have been retrieved, we map them to an array of promises for fetching the variants.
We use Promise.allSettled to wait for all the variants to be retrieved, and then we flatten the result into a single array.
useEffect(()=>(async()=>{
const ps = await getProducts(recipeId)
const variants = takeSuccessful(
await Promise.allSettled(
ps.map(({ProductName})=>getVariants({ recipeId, ProductName }))))
setLoaded(variants.flat())
})())
...and you will need utility functions something like these:
const takeSuccessful = (settledResponses)=>settledResponses.map(({status, value})=>status === 'fulfilled' && value)
const productURL = (recipeId)=>`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
const variantsURL = ({recipeId, productName})=>`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`
const getProducts = async(recipeId)=>
(await axios.get(productURL(recipeId)))?.data?.Products
const getVariants = async({recipeId, productName})=>
(await axios.get(variantsURL({recipeId,productName})))?.data?.Variants