infinite scroll relay modern (v11) using the pagination container and loadMore - reactjs

Using react 17 and relay-modern 11 I want to build a list, with which, when one reaches the end, they can click a button that says load more and it adds more entries to the list. Here is what I have so far. The rows are name and cursor
see I should click load more and it should add an extra 5 rows of data. This is what happens when I click load more
See it did fetch 5 new nodes. I can tell because the cursors are unique. However it did not append it to the list like I wanted.
How can I get this list to continue to build all the time as I keep clicking load more, until there is no more data?
Here is my code:
the root of the component:
// index.js
import React, { useState } from "react";
import { Text, View } from "react-native";
import { QueryRenderer, useRelayEnvironment } from "react-relay";
import PaginatedProfilesListContainer from "./PaginatedProfilesListContainer";
const ProfilesList = () => {
const environment = useRelayEnvironment();
const [count, setCount] = useState(5);
const [name, setName] = useState("");
const query = graphql`
query ProfilesListQuery(
$count: Int!
$cursor: String
$filter: ProfileFilter
) {
...PaginatedProfilesListContainer_list
}
`;
const filter = {
name,
};
const variables = {
count: 5,
cursor: null,
filter,
};
const render = ({ error, props, retry }) => {
if (error) {
return (
<View>
<Text>{error.message}</Text>
</View>
);
} else if (props) {
return (
<View>
<PaginatedProfilesListContainer
pagination={props}
count={count}
setCount={setCount}
name={name}
setName={setName}
filter={filter}
/>
</View>
);
} else {
return (
<View>
<Text>loading profiles list...</Text>
</View>
);
}
};
console.log("vriable", variables)
return (
<QueryRenderer
environment={environment}
query={query}
variables={variables}
render={render}
/>
);
};
export default ProfilesList;
here is the component that should be listing the object
// PaginatedProfilesListContainer.js
import React from "react";
import { Text, View } from "react-native";
import { Button } from "react-native-paper";
import { createPaginationContainer } from "react-relay";
import { FadoTextInput } from "../forms/fadoTextInput";
const PaginatedProfilesListContainer = (props) => {
console.log(props);
console.log("createPaginationContainer", createPaginationContainer)
// console.log(pagination)
const { pagination, count, setCount, name, setName, relay, filter } = props;
const { hasMore, loadMore, refetchConnection } = relay;
console.log(loadMore)
const { profiles } = pagination;
const { edges, pageInfo } = profiles;
return (
<View>
<View>
<FadoTextInput
dense={true}
isNumeric={true}
graphqlErrors={[]}
label="count"
errorKey="count"
// savedValue={price.amount}
value={count}
onChangeText={setCount}
/>
<FadoTextInput
dense={true}
isNumeric={false}
graphqlErrors={[]}
label="name"
errorKey="name"
// savedValue={price.amount}
value={name}
onChangeText={setName}
/>
</View>
{edges.map(({ cursor, node: { name } }) => (
<View key={cursor} style={{ display: "flex", flexDirection: "row"}}>
<Text>{name}</Text>
<Text>{cursor}</Text>
</View>
))}
<Button disabled={!hasMore()} onPress={() => loadMore(count, (error) => { error && console.log("error", error); })}>
Load More
</Button>
</View>
);
};
export default createPaginationContainer(
PaginatedProfilesListContainer,
{
pagination: graphql`
fragment PaginatedProfilesListContainer_list on RootQueryType {
profiles(first: $count, after: $cursor, filter: $filter)
#connection(key: "PaginatedProfilesListContainer_profiles") {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
edges {
cursor
node {
name
}
}
}
}
`,
},
{
direction: 'forward',
query: graphql`
query PaginatedProfilesListContainerQuery(
$count: Int!
$cursor: String
$filter: ProfileFilter
) {
...PaginatedProfilesListContainer_list
}
`,
getConnectionFromProps(props) {
console.log(props)
return props.pagination?.profiles
},
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
filter: {},
};
},
}
);
I got the inspiration for this approach from https://github.com/facebook/relay/issues/1705
Note: I tried to use the #stream_connection, but the Elixir Absinthe on the backend doesn't seem tpo support it.
I know this is long so please any help appreciated :)

Related

How can I change my data fetching strategy to avoid stale data on initial dynamic route renders?

I'm building a Next.js app which allows users to create and view multiple Kanban boards. There are a couple of different ways that a user can view their different boards:
On the Home page of the app, users see a list of boards that they can click on.
The main navigation menu has a list of boards users can click on.
Both use Next.js Link components.
Clicking the links loads the following dynamic page: src/pages/board/[boardId].js The [boardId].js page fetches the board data using getServerSideProps(). An effect fires on route changes, which updates the redux store. Finally, the Board component uses a useSelector() hook to pull the data out of Redux and render it.
The problem I'm experiencing is that if I click back and forth between different boards, I see a brief flash of the previous board's data before the current board data loads. I am hoping someone can suggest a change I could make to my approach to alleviate this issue.
Source code:
// src/pages/board/[boardId].js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Board from 'Components/Board/Board'
import { useRouter } from 'next/router'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
import { unstable_getServerSession } from 'next-auth/next'
import { authOptions } from 'pages/api/auth/[...nextauth]'
import prisma from 'Utilities/PrismaClient'
const BoardPage = ({ board, tasks }) => {
const router = useRouter()
const dispatch = useDispatch()
useEffect(() => {
dispatch(hydrateTasks({ board, tasks }))
}, [router])
return (
<Board />
)
}
export async function getServerSideProps ({ query, req, res }) {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
const { boardId } = query
const boardQuery = prisma.board.findUnique({
where: {
id: boardId
},
select: {
name: true,
description: true,
id: true,
TO_DO: true,
IN_PROGRESS: true,
DONE: true
}
})
const taskQuery = prisma.task.findMany({
where: {
board: boardId
},
select: {
id: true,
title: true,
description: true,
status: true,
board: true
}
})
try {
const [board, tasks] = await prisma.$transaction([boardQuery, taskQuery])
return { props: { board, tasks } }
} catch (error) {
console.log(error)
return { props: { board: {}, tasks: [] } }
}
}
export default BoardPage
// src/Components/Board/Board.js
import { useEffect } from 'react'
import { useStyletron } from 'baseui'
import Column from 'Components/Column/Column'
import ErrorBoundary from 'Components/ErrorBoundary/ErrorBoundary'
import useExpiringBoolean from 'Hooks/useExpiringBoolean'
import { DragDropContext } from 'react-beautiful-dnd'
import Confetti from 'react-confetti'
import { useDispatch, useSelector } from 'react-redux'
import useWindowSize from 'react-use/lib/useWindowSize'
import { moveTask } from 'Redux/Reducers/TaskSlice'
import { handleDragEnd } from './BoardUtilities'
import { StyledBoardMain } from './style'
const Board = () => {
const [css, theme] = useStyletron()
const dispatch = useDispatch()
useEffect(() => {
document.querySelector('body').style.background = theme.colors.backgroundPrimary
}, [theme])
// get data from Redux
const { boardDescription, boardName, columnOrder, columns, tasks } = useSelector(state => state?.task)
// set up a boolean and a trigger to control "done"" animation
const { boolean: showDone, useTrigger: doneUseTrigger } = useExpiringBoolean({ defaultState: false })
const doneTrigger = doneUseTrigger({ duration: 4000 })
// get width and height for confetti animation
const { width, height } = useWindowSize()
// curry the drag end handler for the drag and drop UI
const curriedDragEnd = handleDragEnd({ dispatch, action: moveTask, handleOnDone: doneTrigger })
return (
<ErrorBoundary>
<DragDropContext onDragEnd={curriedDragEnd}>
<div className={css({
marginLeft: '46px',
marginTop: '16px',
fontFamily: 'Roboto',
display: 'flex',
alignItems: 'baseline'
})}>
<h1 className={css({ fontSize: '22px', color: theme.colors.primary })}>{boardName}</h1>
{boardDescription &&
<p className={css({ marginLeft: '10px', color: theme.colors.primary })}>{boardDescription}</p>
}
</div>
<StyledBoardMain>
{columnOrder.map(columnKey => {
const column = columns[columnKey]
const tasksArray = column.taskIds.map(taskId => tasks[taskId])
return (
<Column
column={columnKey}
key={`COLUMN_${columnKey}`}
tasks={tasksArray}
title={column.title}
status={column.status}
/>
)
})}
</StyledBoardMain>
</DragDropContext>
{showDone && <Confetti
width={width}
height={height}
/>}
</ErrorBoundary>
)
}
export default Board
// src/pages/index.tsx
import React, {PropsWithChildren} from 'react'
import {useSelector} from "react-redux";
import {authOptions} from 'pages/api/auth/[...nextauth]'
import {unstable_getServerSession} from "next-auth/next"
import CreateBoardModal from 'Components/Modals/CreateBoard/CreateBoard'
import Link from 'next/link'
import {useStyletron} from "baseui";
const Index: React.FC = (props: PropsWithChildren<any>) => {
const {board: boards} = useSelector(state => state)
const [css, theme] = useStyletron()
return boards ? (
<>
<div style={{marginLeft: '46px', fontFamily: 'Roboto', width: '600px'}}>
<h1 className={css({fontSize: '22px'})}>Boards</h1>
{boards.map(({name, description, id}) => (
<Link href="/board/[boardId]" as={`/board/${id}`} key={id}>
<div className={css({
padding: '20px',
marginBottom: '20px',
borderRadius: '6px',
background: theme.colors.postItYellow,
cursor: 'pointer'
})}>
<h2 className={css({fontSize: '20px'})}>
<a className={css({color: theme.colors.primary, width: '100%', display: 'block'})}>
{name}
</a>
</h2>
<p>{description}</p>
</div>
</Link>
))}
</div>
</>
) : (
<>
<h1>Let's get started</h1>
<button>Create a board</button>
</>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(context.req, context.res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
return {props: {session}}
}
export default Index
It looks like there's only ever one board in the redux. You could instead use a namespace so that you don't have to keep swapping different data in and out of the store.
type StoreSlice = {
[boardId: string]: Board;
}
Then the "brief flash" that you will see will either be the previous data for the correct board, or nothing if it has not yet been fetched.

UseEffect is not working properly and also facing Problem in using it?

I have made an app like Amazon and Flipkart, which filters products based on categories.
import React, { useState,useEffect} from 'react'
import { FlatList,View,Image,Text,StyleSheet,TouchableOpacity,ActivityIndicator} from 'react-native';
import {useSelector , useDispatch} from "react-redux";
import ShopComponent from "../components/ShopComponent";
import * as cartActions from "../store/actions/Cart"
import * as ProductActions from "../store/actions/Product"
import {HeaderButtons,Item} from "react-navigation-header-buttons";
import HeaderButton from "../components/UI/HeaderComponent";
import Product from '../models/Product';
import ProductsList from "../data/DataTemp";
import SearchScreen from "./SearchScreen";
const SecondScreen =(props)=>{
const [CurrentCategory, setCurrentCategory] = useState([]);
const category = props.navigation.getParam("title");
const UpdatedCategory=useSelector(state =>
state.Products.availableProduct.filter((product)=>{
return product.category == category
} )
);
const dispatch=useDispatch();
useEffect(()=>{
setCurrentCategory(UpdatedCategory)
},[])
useEffect(()=>{
dispatch(ProductActions.fetchProduct());
},[dispatch])
const loadBooks = (itemData)=>{
let newBook = {
id : itemData.item.id,
title : itemData.item.title,
Description : itemData.item.Description,
image : itemData.item.image,
Price : itemData.item.Price,
category : itemData.item.category,
}
return (
<TouchableOpacity onPress={()=>{props.navigation.navigate("Detail",
{ productId: itemData.item.id,
productTitle:itemData.item.title } );
}}>
<View style={styles.productMain}>
<View style={{width:"35%", height:200, }}>
<Image style={{width : "100%" , height:"95%" , resizeMode:"contain", borderRadius:5}}
source={{uri : itemData.item.imageUrl}} />
</View>
<View style={{ justifyContent: "space-around", alignContent:"center", marginLeft:40,}}>
<View style={{overFlow:"hidden"}}>
<Text numberOfLines={1} style={styles.text}>{itemData.item.title}</Text>
</View>
<View>
<Text numberOfLines={1} style={styles.text}>Price : Rs {itemData.item.price}</Text>
</View>
</View>
</View>
</TouchableOpacity>
)
}
return (
<View style={styles.main}>
<FlatList
data={CurrentCategory}
keyExtractor={item => item.id}
renderItem={loadBooks}
/>
</View>
)
}
SecondScreen.navigationOptions = navData=>{
return{
headerTitle:"Deep Mart",
headerLeft:(()=><HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item title="ToggleDrawer" iconName={"md-menu" } onPress={()=>{navData.navigation.toggleDrawer();}}/>
</HeaderButtons>
),
headerRight:(()=><HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item title="cart" iconName={"md-cart" } onPress={()=>{navData.navigation.navigate("Cart")}}/>
</HeaderButtons>
), }
};
const styles = StyleSheet.create({
main: {
flex : 1,
padding : 10,
},
bookMain :{
marginTop:10,
width : "100%",
height:500,
borderColor:"black" ,
borderWidth:1,
borderRadius : 5
},
productMain: {
flexDirection:"row",
justifyContent:"flex-start",
borderBottomWidth:1,
marginBottom:10,
backgroundColor:"white",
borderRadius:10,
},
text : {
color:"black",
fontSize: 15,
fontWeight:"bold",
overflow:"hidden",
}
});
export default SecondScreen;
Here I have used two useEffect one for filtering title Based on categories and another for fetching Product from my ProductAction in my store.
import Product from "../../models/Product";
export const DELETE_PRODUCT = "DELETE_PRODUCTS";
export const CREATE_PRODUCT="CREATE_PRODUCT";
export const UPDATE_PRODUCT="UPDATE_PRODUCT";
export const SET_PRODUCTS = "SET_PRODUCTS"
export const SOLD_PRODUCTS = "SOLD_PRODUCTS"
export const fetchProduct=()=>{
return async dispatch => {
// any async code you want!
try {
const response= await fetch("https://direp-marct.firebaseio.com/products.json");
if (!response.ok) {
throw new Error('Something went wrong!');
}
const resData = await response.json();
const loadedproducts = [];
for(const key in resData) {
loadedproducts.push(
new Product(
key ,
"u1",
resData[key].title ,
resData[key].imageUrl ,
resData[key].description,
resData[key].price,
resData[key].mrp,
resData[key].category
)
);
}
dispatch({ type: SET_PRODUCTS, products: loadedproducts });
} catch (err) {
// send to custom analytics server
throw err;
}
};
};
export const deleteProduct=(ProductId)=>{
return async (dispatch) =>{
fetch(`https://direp-marct.firebaseio.com/products/${ProductId}.json`,{
method:"DELETE"
});
dispatch({type:DELETE_PRODUCT , pid:ProductId})
}
};
export const soldProduct=(ProductId)=>{
return async (dispatch) =>{
fetch(`https://direp-marct.firebaseio.com/products/${ProductId}.json`,{
method:"POST"
});
dispatch({type:DELETE_PRODUCT , pid:ProductId})
}
};
export const createProduct=(title , description ,imageUrl, price , mrp ,category)=>{
return async (dispatch)=>{
, {
const response= await fetch("https://direp-marct.firebaseio.com/products.json" ,
{
method:"POST",
header:{
"Content-Type":"Application/json"
},
body:JSON.stringify({
title,
description,
imageUrl,
price,
mrp,
category
})
});
const resData = await response.json();
dispatch({
type:CREATE_PRODUCT,
productData: {
id:resData.name,
title ,
imageUrl ,
description ,
price,
mrp,
category
}});
}
};
export const updateProduct=(id, title , description,imageUrl , price ,mrp, category)=>{
return async (dispatch)=>{
const response=await fetch(`https://direp-marct.firebaseio.com/products/${id}.json`,{
method:"PATCH",
header:{
"Content-type":"application.json"
},
body:JSON.stringify({
title,
description,
imageUrl,
price,
mrp,
category
})
})
dispatch( {
type:UPDATE_PRODUCT,
pid:id ,
productData:{ title,description, imageUrl , price,mrp ,category}}
)
}
};
Here is My Reducer
import PRODUCTS from "../../data/dummy-data";
import {DELETE_PRODUCT , CREATE_PRODUCT , UPDATE_PRODUCT, SET_PRODUCTS , SOLD_PRODUCTS} from "../../store/actions/Product";
import Product from "../../models/Product";
const initialState={
availableProduct : [],
userProduct : []
};
export default (state=initialState , action) =>{
switch (action.type){
case SET_PRODUCTS:
return{
availableProduct:action.products,
userProduct:action.products
}
case CREATE_PRODUCT:
const newProduct = new Product(
action.productData.id,
'u1',
action.productData.title,
action.productData.imageUrl,
action.productData.description,
action.productData.price,
action.productData.mrp,
action.productData.category
);
return {
...state,
availableProduct: state.availableProduct.concat(newProduct),
userProduct: state.userProduct.concat(newProduct)
};
case UPDATE_PRODUCT:
const productIndex = state.userProduct.findIndex(
prod => prod.id === action.pid
);
const updatedProduct = new Product(
action.pid,
state.userProduct[productIndex].ownerId,
action.productData.title,
action.productData.imageUrl,
action.productData.description,
action.productData.price,
action.productData.mrp,
action.productData.category
);
const updatedUserProducts = [...state.userProduct];
updatedUserProducts[productIndex] = updatedProduct;
const availableProductIndex = state.availableProduct.findIndex(
prod => prod.id === action.pid
);
const updatedAvailableProducts = [...state.availableProduct];
updatedAvailableProducts[availableProductIndex] = updatedProduct;
return {
...state,
availableProduct: updatedAvailableProducts,
userProduct: updatedUserProducts
};
case DELETE_PRODUCT:
return {
...state,
userProduct: state.userProduct.filter(
product => product.id !== action.pid
),
availableProduct: state.availableProduct.filter(
product => product.id !== action.pid
)
};
}
return state;
};
The problem is that when I run my app and click on a particular category (for example- Baby Care ) then at first no product is fetched and I see a blank screen. but when I click the second time my product gets fetched and I see the result.
And I want that as I click on the category instead of the blank page I want to see fetched products initially.
I think the problem is with UseEffect and I don't know what to do as I am new to react-native.
Any Help will be appreciated thanks in advance.
The problem is with these two pieces and how they work together:
const UpdatedCategory=useSelector(state =>
state.Products.availableProduct.filter((product)=>{
return product.category == category
} )
);
useEffect(()=>{
setCurrentCategory(UpdatedCategory)
},[])
UpdatedCategory comes from a useSelector hook, which means that it's value gets updated when the state changes.
But this useEffect has an empty dependency array [] which means that the setCurrentCategory gets called only once when the component is mounted.
You want the CurrentCategory to stay updated based on changes in the state, so you need to have UpdatedCategory in your dependencies.
useEffect(()=>{
setCurrentCategory(UpdatedCategory)
},[UpdatedCategory])

Optimistic React Apollo ui lag with React Beautiful Drag and Drop

I'm trying to create a optimistic response where the ui updates immmediately (minimal lag and better user experience) with drag and dropped data. The issue i'm having however is that it lags anyways.
So whats happening is that I expect a list of zones and unassigned zones from my query, unassignedZone is a object, with cities in them, and zones is a list of zones with cities within them. When writing my mutation, I return the new reordered list of zones after dragging and dropping them. The reordering is done by a field on a zone object called 'DisplayOrder' The logic is setting the numbers correct. The problem is that when I try to mimic it with optimistic ui and update, there is a slight lag like its still waiting for the network.
Most of the meat of what i'm trying to achieve is happening at the onDragEnd = () => { ... } function.
import React, { Component } from "react";
import { graphql, compose, withApollo } from "react-apollo";
import gql from "graphql-tag";
import { withState } from "recompose";
import { withStyles } from "#material-ui/core/styles";
import Select from "#material-ui/core/Select";
import MenuItem from "#material-ui/core/MenuItem";
import Input from "#material-ui/core/Input";
import Grid from "#material-ui/core/Grid";
import InputLabel from "#material-ui/core/InputLabel";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
import AppBar from "#material-ui/core/AppBar";
import _ from "lodash";
import FormControl from "#material-ui/core/FormControl";
import move from "lodash-move";
import { Zone } from "../../Components/Zone";
const style = {
ddlRight: {
left: "3px",
position: "relative",
paddingRight: "10px"
},
ddlDrop: {
marginBottom: "20px"
},
dropdownInput: {
minWidth: "190px"
}
};
class Zones extends Component {
constructor(props) {
super(props);
this.state = {
companyId: "",
districtId: "",
selectedTab: "Zones",
autoFocusDataId: null,
zones: [],
unassignedZone: null
};
}
handleChange = event => {
const { client } = this.props;
this.setState({ [event.target.name]: event.target.value });
};
handleTabChange = (event, selectedTab) => {
this.setState({ selectedTab });
};
onDragStart = () => {
this.setState({
autoFocusDataId: null
});
};
calculateLatestDisplayOrder = () => {
const { allZones } = this.state;
if (allZones.length === 0) {
return 10;
}
return allZones[allZones.length - 1].displayOrder + 10;
};
updateCitiesDisplayOrder = cities => {
let displayOrder = 0;
const reorderedCities = _.map(cities, aCity => {
displayOrder += 10;
const city = { ...aCity, ...{ displayOrder } };
if (city.ZonesCities) {
city.ZonesCities.displayOrder = displayOrder;
}
return city;
});
return reorderedCities;
};
moveAndUpdateDisplayOrder = (allZones, result) => {
const reorderedZones = _.cloneDeep(
move(allZones, result.source.index, result.destination.index)
);
let displayOrder = 0;
_.each(reorderedZones, (aZone, index) => {
displayOrder += 10;
aZone.displayOrder = displayOrder;
});
return reorderedZones;
};
/**
* droppable id board represents zones
* #param result [holds our source and destination draggable content]
* #return
*/
onDragEnd = result => {
console.log("Dragging");
if (!result.destination) {
return;
}
const source = result.source;
const destination = result.destination;
if (
source.droppableId === destination.droppableId &&
source.index === destination.index
) {
return;
}
const {
zonesByCompanyAndDistrict,
unassignedZoneByCompanyAndDistrict
} = this.props.zones;
// reordering column
if (result.type === "COLUMN") {
if (result.source.index < 0 || result.destination.index < 0) {
return;
}
const { reorderZones, companyId, districtId } = this.props;
const sourceData = zonesByCompanyAndDistrict[result.source.index];
const destinationData =
zonesByCompanyAndDistrict[result.destination.index];
const reorderedZones = this.moveAndUpdateDisplayOrder(
zonesByCompanyAndDistrict,
result
);
console.log(reorderedZones);
console.log(unassignedZoneByCompanyAndDistrict);
reorderZones({
variables: {
companyId,
districtId,
sourceDisplayOrder: sourceData.displayOrder,
destinationDisplayOrder: destinationData.displayOrder,
zoneId: sourceData.id
},
optimisticResponse: {
__typename: "Mutation",
reorderZones: {
zonesByCompanyAndDistrict: reorderedZones
}
},
// refetchQueries: () => ["zones"],
update: (store, { data: { reorderZones } }) => {
const data = store.readQuery({
query: unassignedAndZonesQuery,
variables: {
companyId,
districtId
}
});
store.writeQuery({
query: unassignedAndZonesQuery,
data: data
});
}
});
// this.setState({ zones: reorderedZones });
// Need to reorder zones api call here
// TODO: Elixir endpoint to reorder zones
}
return;
};
render() {
const { selectedTab } = this.state;
const {
classes,
companies,
districts,
companyId,
districtId,
setCompanyId,
setDistrictId,
zones
} = this.props;
const isDisabled = !companyId || !districtId;
return (
<Grid container spacing={16}>
<Grid container spacing={16} className={classes.ddlDrop}>
<Grid item xs={12} className={classes.ddlRight}>
<h2>Company Zones</h2>
</Grid>
<Grid item xs={2} className={classes.ddlRight}>
<FormControl>
<InputLabel htmlFor="company-helper">Company</InputLabel>
<Select
value={companyId}
onChange={event => {
setCompanyId(event.target.value);
}}
input={
<Input
name="companyId"
id="company-helper"
className={classes.dropdownInput}
/>
}
>
{_.map(companies.companies, aCompany => {
return (
<MenuItem
value={aCompany.id}
key={`companyItem-${aCompany.id}`}
>
{aCompany.name}
</MenuItem>
);
})}
</Select>
</FormControl>
</Grid>
<Grid item xs={2} className={classes.ddlRight}>
<FormControl>
<InputLabel htmlFor="district-helper">District</InputLabel>
<Select
value={districtId}
onChange={event => {
setDistrictId(event.target.value);
}}
input={
<Input
name="districtId"
id="district-helper"
className={classes.dropdownInput}
/>
}
>
{_.map(districts.districts, aDistrict => {
return (
<MenuItem
value={aDistrict.id}
key={`districtItem-${aDistrict.id}`}
>
{aDistrict.name}
</MenuItem>
);
})}
</Select>
</FormControl>
</Grid>
</Grid>
<Grid container>
<AppBar position="static" color="primary">
<Tabs value={selectedTab} onChange={this.handleTabChange}>
<Tab value="Zones" label="Zone" disabled={isDisabled} />
<Tab
value="Pricing Structure"
label="Pricing Structure"
disabled={isDisabled}
/>
<Tab value="Pricing" label="Pricing" disabled={isDisabled} />
<Tab
value="Student Pricing"
label="Student Pricing"
disabled={isDisabled}
/>
</Tabs>
</AppBar>
{selectedTab === "Zones" &&
zones &&
zones.zonesByCompanyAndDistrict && (
<Zone
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
zones={_.sortBy(zones.zonesByCompanyAndDistrict, [
"displayOrder"
])}
unassignedZone={zones.unassignedZoneByCompanyAndDistrict}
/>
)}
{selectedTab === "Pricing Structure" && <div>Pricing Structure</div>}
{selectedTab === "Pricing" && <div>Pricing</div>}
{selectedTab === "Student Pricing" && <div>Student Pricing</div>}
</Grid>
</Grid>
);
}
}
const companiesQuery = gql`
query allCompanies {
companies {
id
name
}
}
`;
const districtsQuery = gql`
query allDistricts {
districts {
id
name
}
}
`;
const unassignedAndZonesQuery = gql`
query zones($companyId: String!, $districtId: String!) {
unassignedZoneByCompanyAndDistrict(
companyId: $companyId
districtId: $districtId
) {
name
description
displayOrder
cities {
id
name
}
}
zonesByCompanyAndDistrict(companyId: $companyId, districtId: $districtId) {
id
name
description
displayOrder
basePrice
zoneCities {
displayOrder
city {
id
name
}
}
}
}
`;
const reorderZones = gql`
mutation(
$companyId: String!
$districtId: String!
$sourceDisplayOrder: Int!
$destinationDisplayOrder: Int!
$zoneId: String!
) {
reorderZones(
companyId: $companyId
districtId: $districtId
sourceDisplayOrder: $sourceDisplayOrder
destinationDisplayOrder: $destinationDisplayOrder
zoneId: $zoneId
) {
id
__typename
name
description
displayOrder
basePrice
zoneCities {
displayOrder
city {
id
name
}
}
}
}
`;
export default compose(
withState("companyId", "setCompanyId", ""),
withState("districtId", "setDistrictId", ""),
graphql(unassignedAndZonesQuery, {
name: "zones",
skip: ({ companyId, districtId }) => !(companyId && districtId),
options: ({ companyId, districtId }) => ({
variables: { companyId, districtId },
fetchPolicy: "cache-and-network"
})
}),
graphql(companiesQuery, {
name: "companies",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(districtsQuery, {
name: "districts",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(reorderZones, {
name: "reorderZones"
}),
withApollo,
withStyles(style)
)(Zones);
https://drive.google.com/file/d/1ujxTOGr0YopeBxrGfKDGfd1Cl0HiMaK0/view?usp=sharing <- this is a video demonstrating it happening.
For anyone who comes across this same issue, the main problem was that my update / optimisticResponse were both not correct. Something to mention here is this block:
update: (store, { data: { reorderZones } }) => {
const {
zonesByCompanyAndDistrict,
unassignedZoneByCompanyAndDistrict
} = store.readQuery({
query: unassignedAndZonesQuery,
variables: {
companyId,
districtId
}
});
const reorderedZones = this.moveAndUpdateDisplayOrder(
zonesByCompanyAndDistrict,
result
);
store.writeQuery({
query: unassignedAndZonesQuery,
variables: { companyId, districtId },
data: {
unassignedZoneByCompanyAndDistrict,
zonesByCompanyAndDistrict: reorderedZones
}
});
}
If you compare it to my original code up top, you see that when I writeQuery I have variables this time. Looking with the apollo devtools, I saw that there was a entry added, just one with the wrong variables. So that was a easy fix. The optimistic response was correct (mimics the payload returned from our mutation). The other aspect that was wrong, was that my query for fetching all this data initially had a fetch policy of cache and network, what this means is that when we receive data we cache it, and we ask for the latest data. So this will always fetch the latest data. I didn't need that, hence the little lag coming, I just needed optimisticResponse. By default apollo does cache-first, where it looks in the cache for data, if its not there we grab it via the network. Pairs well with cache updates and slow nets.

How to change each of state of list item separately in react redux?

Using
react-redux
redux-persist
redux-actions
react-native
I'm learning react-redux and react-native by myself.
I'm trying to update a single item from list in react-redux now.
There's many categories and stores in my project.
An user would select one of categories, and then click 'like button' of one item in store list.
It's like instagram or facebook.
When I change one item's state, the state of every item in the store list change at the same time.
I have no idea why it happens.
I set the structure to ducks pattern to avoid change too much files when to change state.
If anyone give some advice, I would appreciate and it could be helpful for me. Thank you.
I saw some article to resolve this issue, have to give id to distinguish items and make the payload as object. I didn't understand well, so my code is messy now. But I'd like to know what happen to my code, so I share my code.
restaurantContainer.js
class RestaurantListContainer extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.imgUrls !== this.props.imgUrls;
}
componentDidMount() {
const {category, StoreActions} = this.props;
try {
StoreActions.fetchStoreInfo(category);
StoreActions.fetchImgUrl(category);
this.getUid();
} catch (e) {
console.log(e);
}
}
async getUid() {
const {StoreActions} = this.props;
const uid = await storage.getItem('uid');
StoreActions.fetchLikeList(uid);
}
render() {
const {fetching, tabColor, tabName, category, categoryId, navigation, stores, imgUrls, like} = this.props;
const {onClick} = this;
return (
<View>
...
<ScrollView>
{
fetching ?
<View>
<Bars size={30} color="#40D59B"/>
</View>
:
stores.map((store, i) =>
<View key={`item-${i}`}>
<RestaurantItem
key={`restaurantItem-${i}`}
i={i}
category={category}
navigation={navigation}
storeInfo={store}
imgUrls={imgUrls[i]}
categoryId={categoryId}
like={like}
/>
</View>
)
}
</ScrollView>
</View>
);
}
}
export default connect(
({styleMod, storeMod}) => ({
stores: storeMod.stores,
imgUrls: storeMod.imgUrls,
fetching: storeMod.fetching,
likeList: storeMod.likeList
}),
(dispatch) => ({
StoreActions: bindActionCreators(storeActions, dispatch),
})
)(RestaurantListContainer);
restaurantItem.js
class RestaurantItem extends Component {
pressFunc = (item) => {
const {navigation} = this.props;
const {push} = navigation;
console.log(item.name);
push('RestaurantDetail', {info: item});
}
voteAdder = async (storeName) => {
const uid = await storage.getItem('uid');
const {i, categoryId} = this.props;
if (uid) {
const {VoteActions, LikeActions, category, favoriteStores} = this.props;
try {
VoteActions.voteAdd(favoriteStores, category, storeName, uid);
LikeActions.likeClicked(storeName, category, categoryId, i);
} catch (e) {
console.log(e);
}
} else {
alert('You are not authorized!');
}
}
render() {
const {i, storeInfo, category, categoryId, imgUrls, favoriteStores, like} = this.props;
return (
<View style={restaurantCard}>
<StoreImg
img={imgUrls}
name={storeInfo.name}
/>
<StoreInfoBlock
i={i}
storeInfo={storeInfo}
pressFunc={this.pressFunc}
/>
<View style={{flexDirection: 'column'}} >
{
<ThumbImg
voteAdder={() => this.voteAdder(storeInfo.name)}
name={storeInfo.name}
favoriteStore={favoriteStores[category]}
category={category}
like={like}
categoryId={categoryId}
/>
}
<Score count={storeInfo.count}/>
</View>
</View>
);
}
}
export default connect(
({voterMod, likeMod}) => ({
favoriteStores: voterMod.favoriteStores,
like: likeMod.like,
}),
(dispatch) => ({
VoteActions: bindActionCreators(voteActions, dispatch),
LikeActions: bindActionCreators(likeActions, dispatch),
})
)(RestaurantItem);
thumbImg.js
export default class ThumbImg extends Component {
onClick = () => {
this.props.voteAdder();
}
onFlag = () => {
const {like, categoryId, i} = this.props;
if(like.categoryById[categoryId]) {
if(like.storeById[i]) {
console.log(2);
return (
<FastImage
resizeMode={FastImage.resizeMode.cover}
style={{width: 50, height: 50}}
source={require('...')}
/>
);
} else {
return (
<FastImage
resizeMode={FastImage.resizeMode.cover}
style={{width: 50, height: 50}}
source={require('...')}
/>
);
}
} else {
return (
<FastImage
resizeMode={FastImage.resizeMode.cover}
style={{width: 50, height: 50}}
source={require('...')}
/>
);
}
}
render() {
return (
<TouchableOpacity onPress={this.onClick}>
<View style={{paddingTop: 15, paddingRight: 15}}>
{
this.onFlag()
}
</View>
</TouchableOpacity>
);
}
}
likeMod.js
// Action types
const ON_LIKE = 'like/ON_LIKE';
const OFF_LIKE = 'like/OFF_LIKE';
// action creator
export const likeClicked = (store, category, categoryId, i) => (dispatch) => {
const selectedCategory = {categoryById: {}};
const categoryInfo = {
id: categoryId,
name: category,
};
selectedCategory.categoryById[categoryId] = categoryInfo;
const selectedStore = {storeById: {}};
const storeInfo = {
id: i,
name: store
}
selectedStore.storeById[i] = storeInfo;
const favorites = {
...selectedCategory,
...selectedStore
}
dispatch({type: ON_LIKE, payload: favorites});
}
const initialState = {
like: {
categoryById: {},
storeById: {}
}
};
// Reducer
export default handleActions({
[ON_LIKE]: (state, action) => ({...state, like: action.payload}),
[OFF_LIKE]: (state, action) => ({...state, like: action.payload}),
}, initialState);

How data.refetch() function from react-apollo works

On the frontend, I am using ReactJS and trying to build-in a filtering option to a list view. The list view correctly getting data from graphql endpoint by issuing this graphql query:
query getVideos($filterByBook: ID, $limit: Int!, $after: ID) {
videosQuery(filterByBook: $filterByBook, limit: $limit, after: $after) {
totalCount
edges {
cursor
node {
id
title
ytDefaultThumbnail
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
On the initial load $filterByBook variable is set to null, so the query correctly returns all pages for all nodes (query returns a paginated result). Then, by clicking on the filter (filter by book) another graphql query is issuing, but it always returns the same data. Here is a code snippet for filtering component
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
And, here is complete code without imports for the list view component
class VideoList extends React.Component {
constructor(props) {
super(props);
this.subscription = null;
}
componentWillUnmount() {
if (this.subscription) {
// unsubscribe
this.subscription();
}
}
renderVideos() {
const { videosQuery } = this.props;
return videosQuery.edges.map(({ node: { id, title, ytDefaultThumbnail } }) => {
return (
<Col sm="4" key={id}>
<Card>
<CardImg top width="100%" src={ytDefaultThumbnail} alt="video image" />
<CardBlock>
<CardTitle>
<Link
className="post-link"
to={`/video/${id}`}>
{title}
</Link>
</CardTitle>
</CardBlock>
</Card>
</Col>
);
});
}
renderLoadMore() {
const { videosQuery, loadMoreRows } = this.props;
if (videosQuery.pageInfo.hasNextPage) {
return (
<Button id="load-more" color="primary" onClick={loadMoreRows}>
Load more ...
</Button>
);
}
}
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
render() {
const { loading, videosQuery } = this.props;
if (loading && !videosQuery) {
return (
<div>{ /* loading... */}</div>
);
} else {
return (
<div>
<Helmet
title="Videos list"
meta={[{
name: 'description',
content: 'List of all videos'
}]} />
<h2>Videos</h2>
{this.renderFilters()}
<Row>
{this.renderVideos()}
</Row>
<div>
<small>({videosQuery.edges.length} / {videosQuery.totalCount})</small>
</div>
{this.renderLoadMore()}
</div>
);
}
}
}
export default compose(
graphql(VIDEOS_QUERY, {
options: () => {
return {
variables: {
limit: 3,
after: 0,
filterByBook: null
},
};
},
props: ({ data }) => {
const { loading, videosQuery, fetchMore, subscribeToMore, refetch } = data;
const loadMoreRows = () => {
return fetchMore({
variables: {
after: videosQuery.pageInfo.endCursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const totalCount = fetchMoreResult.videosQuery.totalCount;
const newEdges = fetchMoreResult.videosQuery.edges;
const pageInfo = fetchMoreResult.videosQuery.pageInfo;
return {
videosQuery: {
totalCount,
edges: [...previousResult.videosQuery.edges, ...newEdges],
pageInfo,
__typename: "VideosQuery"
}
};
}
});
};
return { loading, videosQuery, subscribeToMore, loadMoreRows, refetch };
}
}),
graphql(LIST_BOOKS_QUERY, {
props: ({ data }) => {
const { listOfBooksWithChapters } = data;
return { listOfBooksWithChapters };
}
}),
)(VideoList);
Question:
Why refetch function returns data without taking into account new variable filterByBook? How to check which variables object I supplied to the refetch function? Do I need to remap data that I receive from refetch function back to the component props?
EDIT:
I found the way to find what variable object I supplied to the query and found that variable object on filtering event returns this data
variables:Object
after:0
limit:3
variables:Object
after:0
filterByBook:"2"
limit:3
you can do it by adding refetch in useQuery
const {loading, data, error,refetch} = useQuery(GET_ALL_PROJECTS,
{
variables: {id: JSON.parse(localStorage.getItem("user")).id}
});
then call function refetch()
const saveChange = input => {
refetch();
};
OR
const saveChange = input => {
setIsOpen(false);
addProject({
variables: {
createBacklogInput: {
backlogTitle: backlogInput,
project:id
}
}
}).then(refetch);
It seem that refetch function is not meant to refetch data with different variables set (see this discussion).
I finally and successfully solved my issue with the help from this article

Resources