Currently I have a BooksList component and I'm passing down props to my BooksDetails component when a title is clicked. How do I use an Apollo hook to only query on props change?
I'm not sure how to do this using hooks. I've looked through the UseQuery documentation from Apollo. I couldn't find documentation on UseLazyQuery.
I've tried the following, but it always returns undefined:
const { loading, error, data } = useQuery(getBookQuery, {
options: (props) => {
return {
variables: {
id: props.bookId
}
}
}
})
BookList:
const BookList = () => {
const {loading, error, data} = useQuery(getBooksQuery)
const [selectedId, setId] = useState('');
return (
<div id='main'>
<ul id='book-list'>
{data && data.books.map(book => (
<li onClick={() => setId(book.id)} key={book.id}>{book.name}</li>
)) }
</ul>
<BookDetails bookId={selectedId} />
</div>
);
};
export default BookList;
BookDetails:
const BookDetails = (props) => {
const { loading, error, data } = useQuery(getBookQuery, {
options: (props) => {
return {
variables: {
id: props.bookId
}
}
}
})
console.log(data)
return (
<div id='book-details'>
<p>Output Book Details here</p>
</div>
);
};
export default BookDetails;
EDIT - I forgot to add that my GetBookQuery has a parameter of ID so an example would be getBookQuery(123).
Use the useLazyQuery like this instead:
const [getBook, { loading, error, data }] = useLazyQuery(getBooksQuery);
Full example:
import React from 'react';
import { useLazyQuery } from '#apollo/react-hooks';
const BookList = () => {
const [getBook, { loading, error, data }] = useLazyQuery(getBooksQuery);
return (
<div id='main'>
<ul id='book-list'>
{data && data.books.map(book => (
<li onClick={() => getBook({ variables: { id: book.id } })}} key={book.id}>{book.name}</li>
)) }
</ul>
<BookDetails data={data} />
</div>
);
};
export default BookList;
Examples and documentation can be found in Apollo's GraphQL documentation
https://www.apollographql.com/docs/react/data/queries/
I was also following along the same example and I came up here with the same question. I tried doing the following way. This might be helpful for someone.
BookDetails.js:
function BookDetails({ bookId }) {
const [loadDetails, { loading, error, data }] = useLazyQuery(getBook);
useEffect(() => {
if (bookId) {
loadDetails({ variables: { id: bookId } });
}
}, [bookId, loadDetails]);
if (!bookId) return null;
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
// for example purpose
return <div>{JSON.stringify(data)}</div>;
}
export default BookDetails;
Related
How to pass data inside single product page ??
const store = configureStore({
reducer: {
cart: CartReducer,
product: ProductReducer,
},
});
productSlice
import { createSlice } from "#reduxjs/toolkit";
import { STATUS } from "./StatusSlice";
const ProductSlice = createSlice({
name: "product",
initialState: {
data: [],
status: STATUS.IDLE,
},
reducers: {
setProducts(state, action) {
state.data = action.payload;
},
setStatus(state, action) {
state.status = action.payload;
},
},
});
export const { setProducts, setStatus } = ProductSlice.actions;
export default ProductSlice.reducer;
export function fetchProducts() {
return async function fetchProduct(dispatch) {
dispatch(setStatus(STATUS.LOADING));
try {
const res = await fetch("https://fakestoreapi.com/products#");
const data = await res.json();
dispatch(setProducts(data));
dispatch(setStatus(STATUS.IDLE));
} catch (error) {
console.log(error);
dispatch(setStatus(STATUS.ERROR));
}
};
}
here is product page
const Products = () => {
const dispatch = useDispatch();
const { data: products, status } = useSelector((state) => state.product);
useEffect(() => {
dispatch(fetchProducts());
}, []);
if (status === STATUS.LOADING) {
return <h1>Loading......!!</h1>;
}
if (status === STATUS.ERROR) {
return <h1>Something went wrong</h1>;
}
return (
<>
<div className={style.wrapper}>
{products.map((product) => {
return (
<Link to={`/products/${product.id}`} key={product.id}>
// rest of the codes
</Link>
);
})}
</div>
</>
);
};
i'm not able to understand how get data in single product page
const SingleProduct = () => {
return (
// details
);
};
i had two option,
i can fetch api data in products page, single product page and wherever i want to or
i can fetch api data in one page (productSlice) and use everywhere that's why redux has been made.
Need Solution :
You are passing the product id as part of the URL path:
{products.map((product) => {
return (
<Link to={`/products/${product.id}`} key={product.id}>
// rest of the codes
</Link>
);
})}
Assuming you have a router and a route for path="/products/:productId" rendering the SingleProduct component:
<Routes>
...
<Route path="/products/:productId" element={<SingleProduct />} />
...
</Routes>
Then the SingleProduct component can use the useParams hook to access the productId route path parameter value and use this to filter the products array selected from the redux store.
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
const SingleProduct = () => {
const products = useSelector(state => state.product.data);
const { productId } = useParams();
const product = products.find(product => String(product.id) === productId);
if (!product) {
return "Sorry, no matching product found.";
}
return (
// render product details
);
};
I'm using Redux Toolkit for the first time. Data is successfully available in the console, but when i try to render data in the UI, i'm getting undefined JSON path {${weather[0].description} ${weather[0].main}} Maybe i need to check something with if() statement but i don't know how and where. My own if() solution didn't do the trick in App.js
JSON data
description: "broken clouds"
icon: "04n"
id: 803
main: "Clouds"
[[Prototype]]: Object
length: 1
[[Prototype]]: Array(0)
App.js side
import { useDispatch, useSelector } from 'react-redux';
import { fetchWeatherAction } from './redux/slices/weatherSlices';
function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchWeatherAction('Seoul'));
}, []);
const state = useSelector(state => state.weather);
const { loading, weather, error } = state || {};
if(!weather){
return null
}
console.log(weather);
return (
<div className="App">
<header className="App-header">
{weather.map((weather, index) => (
<div key={index}>
<div>{`${weather[0].description} ${weather[0].main}`}</div>
</div>
))}
</header>
</div>
);
}
export default App;```
Redux Toolkit side
``` import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import axios from 'axios';
export const fetchWeatherAction = createAsyncThunk(
'weather/fetch',
async (payload, {rejectWithValue, getState, dispatch})=>{
try{
const {data} = await axios.get(`http://api.openweathermap.org/data/2.5/weather?q=${payload}&appid=7469e38d322111e34a7027db2eee39c3`);
return data;
}catch(error){
if(!error?.response){
throw error
}
return rejectWithValue(error?.response?.data);
}
}
);
const weatherSlice = createSlice({
name: 'weather',
initialState: {},
extraReducers: builder => {
builder.addCase(fetchWeatherAction.pending, (state, action) => {
state.loading = true;
});
builder.addCase(fetchWeatherAction.fulfilled, (state, action) => {
state.weather = action?.payload;
state.loading = false;
state.error = undefined;
});
builder.addCase(fetchWeatherAction.rejected, (state, action) => {
state.loading = false;
state.weather = undefined;
state.error = action?.payload;
})
},
});
export default weatherSlice.reducer;```
It appears that you are mapping weather, which looks like an array of objects, and then trying to index into that object as e.g. weather[0].... If weather inside the map operation is in fact an object and not an array, this will not work. I think what you want is something like the following. Note that I've changed the name of the interior variable to weatherItem for clarity:
{weather.map((weatherItem, index) => (
<div key={index}>
<div>{`${weatherItem.description} ${weatherItem.main}`}</div>
</div>
))}
So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example
I'm trying to render data from props in React functional component that look like this:
interface TagsComponentProps {
tags: Tag[];
}
const TagsComponent: FC<TagsComponentProps> = (props: TagsComponentProps) => (
<>
{props.tags.length === 0 &&
<LoadingStateComponent />
}
{props.tags.map(tag => {
{ tag.tagId }
{ tag.tagName }
})
}
</>
)
export default TagsComponent;
Within Next.js page that receiving data inside the getStaticProps method. It looks like that:
const IndexPage = ({ tags }: InferGetStaticPropsType<typeof getStaticProps>) => (
<>
<LayoutComponent>
<TagsComponent tags={tags} />
</LayoutComponent>
</>
)
export default IndexPage;
export const getStaticProps = async () => {
const res = await fetch(`${process.env.HOST}/api/tags/read`)
const data = await res.json()
// if (error) {
// return <ErrorComponent errorMessage={'Ошибка загрузки тегов'} />
// }
return {
props: {
tags: data.Items as Tag[]
}
}
}
But nothing is getting rendered at all although I'm receiving data. Probably I'm missing some concept of data fetching for SSR in Next.js.
I guess the issue is .map() is not returning anything in your code here:
{props.tags.map(tag => {
{ tag.tagId }
{ tag.tagName }
})
}
Instead you should try as the following:
{
props.tags.map(tag => (
<>
{ tag.tagId }
{ tag.tagName }
</>
))
}
Also you can do a null check before as props.tags && props.tags.map().
I am currently working with react, redux, and firebase to display a list of available books for sale. I have successfully populated an array with all the information I want to display and updated the state for redux with this information. However, the HTML does not change from a blank screen.
I have posted some relevant code below.
import React, { Component } from 'react';
import NavBar from './NavBar'
import {populate} from '../actions'
import {useSelector, useDispatch} from 'react-redux'
const ItemList = () => {
const itemList = useSelector(state => state.listReducer);
const dispatch = useDispatch();
if (!itemList.isLoaded) {
dispatch(populate());
}
//Displays all items for sale
return(
<div>
<header>
<div className='wrapper'>
</div>
</header>
<div className='container'>
<section className='display-item'>
<div className="wrapper">
<ul>
{itemList.isLoaded ? itemList.items.map((item) => {
return (
<li key={item.id}>
<h3 key={item.title}>{item.title}</h3>
<p key={item.author}>Author: {item.author}
<button key={"button:" + item.id}>See item details</button>
</p>
</li>
)
}) : null}
</ul>
</div>
</section>
</div>
</div>
);
}
export default ItemList
Reducer code below
const initialState = {
items: [],
isLoaded: false,
}
const listReducer = (state=initialState,action) => {
switch (action.type) {
case 'POPULATE-LIST':
return {
items: action.payload,
isLoaded: true,
}
case 'ADD-ITEM':
let newState = state.slice();
newState.push(action.payload);
return newState
default: {
return state;
}
}
}
Action method below
export const populate = () => (
dispatch,
getState,
{getFirebase}) => {
const firebase = getFirebase();
let items = [];
const itemsRef = firebase.firestore().collection("items").doc("all-items").collection("books");
itemsRef.get().then(documentSnapshot => {
documentSnapshot.forEach(doc => {
const item = doc.data();
items.push(item);
})
}).then(dispatch({type: 'POPULATE-LIST', payload: items}))
}
In addition, I am currently also using redux-thunk
Try putting 'let items' within the promise call to firebase, and then instead of putting the dispatch in a separate promise, put it outside of the forEach loop but within the initial promise. The code will look like this:
export const populate = () => (
dispatch,
getState,
{getFirebase}) => {
const firebase = getFirebase();
const itemsRef = firebase.firestore().collection("items").doc("all-items").collection("books");
itemsRef.get().then(documentSnapshot => {
const items = [];
documentSnapshot.forEach(doc => {
const item = doc.data();
items.push(item);
})
dispatch({type: 'POPULATE-LIST', payload: items})
});
} ```