React modal won't execute GraphQL query - reactjs

I was given a task to update and improve the UI of an existing React app without prior knowledge to GraphQL, so it's my first time. It's a simple app where you can edit and delete items. My task is to style and add a react-modal functionality for the Edit and Update forms. After adding the features the modal works as well as the changehandler for each form but the edit and delete features doesn't work anymore. Need help.
Here's the old code for the List item without react-modal:
import React, { useState } from 'react';
import styled from 'styled-components';
import { EXECUTE } from '#nostack/no-stack';
import compose from '#shopify/react-compose';
import { graphql } from '#apollo/react-hoc';
import {
UPDATE_ITEM_FOR_LIST_ACTION_ID,
DELETE_ITEM_FOR_LIST_ACTION_ID,
} from '../../../config';
import EditInstanceForm from '../../EditInstanceForm';
import DeleteInstanceMenu from '../../DeleteInstanceMenu';
import ItemStyleWrapper from '../../Items';
import Button from '../../Items';
function Item({
item,
parentId,
selected,
updateInstance,
deleteInstance,
refetchQueries,
onSelect,
}) {
const [itemValue, updateItemValue] = useState(item.value);
const [isEditMode, updateIsEditMode] = useState(false);
const [isSaving, updateIsSaving] = useState(false);
const [isDeleteMode, updateIsDeleteMode] = useState(false);
const [isDeleting, updateIsDeleting] = useState(false);
if (!selected) {
return (
<ItemStyleWrapper onClick={() => onSelect(item.id)}>
{itemValue}
</ItemStyleWrapper>
);
}
function handleItemValueChange(e) {
updateItemValue(e.target.value);
}
async function handleItemValueSave() {
updateIsSaving(true);
await updateInstance({
variables: {
actionId: UPDATE_ITEM_FOR_LIST_ACTION_ID,
executionParameters: JSON.stringify({
value: itemValue,
instanceId: item.id,
}),
},
refetchQueries,
});
updateIsEditMode(false);
updateIsSaving(false);
console.log('Saving');
}
function handleCancelEdit() {
updateIsEditMode(false);
}
if (isEditMode) {
return (
<ItemStyleWrapper>
<EditInstanceForm
id={item.id}
label='Item Value:'
value={itemValue}
onChange={handleItemValueChange}
onSave={handleItemValueSave}
onCancel={handleCancelEdit}
disabled={isSaving}
/>
</ItemStyleWrapper>
);
}
async function handleDelete() {
updateIsDeleting(true);
try {
await deleteInstance({
variables: {
actionId: DELETE_ITEM_FOR_LIST_ACTION_ID,
executionParameters: JSON.stringify({
parentInstanceId: parentId,
instanceId: item.id,
}),
},
refetchQueries,
});
} catch (e) {
updateIsDeleting(false);
}
console.log('Deleting');
}
function handleCancelDelete() {
updateIsDeleteMode(false);
}
if (isDeleteMode) {
return (
<ItemStyleWrapper selected={selected} isDeleting={isDeleting}>
{itemValue}
<DeleteInstanceMenu
onDelete={handleDelete}
onCancel={handleCancelDelete}
disabled={isDeleting}
/>
</ItemStyleWrapper>
);
}
return (
<ItemStyleWrapper selected={selected}>
{itemValue}
<Button type='button' onClick={() => updateIsEditMode(true)}>
✎
</Button>
<Button type='button' onClick={() => updateIsDeleteMode(true)}>
🗑
</Button>
</ItemStyleWrapper>
);
}
export default compose(
graphql(EXECUTE, { name: 'updateInstance' }),
graphql(EXECUTE, { name: 'deleteInstance' })
)(Item);
Here's the new and updated code of the same component:
import React, { useState } from 'react';
import styled from 'styled-components';
import { EXECUTE } from '#nostack/no-stack';
import compose from '#shopify/react-compose';
import { graphql } from '#apollo/react-hoc';
import ModalContainer from '../../Modals/ModalContainer';
import '../../Modals/modals.css';
import { FaCheck, FaTrashAlt, FaEdit } from 'react-icons/fa';
import {
UPDATE_ITEM_FOR_LIST_ACTION_ID,
DELETE_ITEM_FOR_LIST_ACTION_ID,
} from '../../../config';
import EditInstanceForm from '../../EditInstanceForm';
import DeleteInstanceMenu from '../../DeleteInstanceMenu';
import ItemStyleWrapper from '../../Items';
import Button from '../../Items';
import ItemCheckBox from '../../Items';
import ItemValueContainer from '../../Items';
function Item({
item,
parentId,
selected,
updateInstance,
deleteInstance,
refetchQueries,
onSelect,
}) {
const [itemValue, updateItemValue] = useState(item.value);
const [editModal, setEditModal] = useState(false);
const [isSaving, updateIsSaving] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
const [isDeleting, updateIsDeleting] = useState(false);
const [isChecked, updateIsChecked] = useState(false);
const handleEditModal = () => setEditModal((prevState) => !prevState);
const handleDeleteModal = () => setDeleteModal((prevState) => !prevState);
if (!selected) {
return (
<ItemStyleWrapper>
<ItemCheckBox htmlFor={item.id}>
<input
type='checkbox'
id={item.id}
checked={isChecked}
onChange={handleItemCheckedStatus}
/>
<FaCheck />
</ItemCheckBox>
<ItemValueContainer
onClick={() => onSelect(item.id)}
isChecked={isChecked}
>
{itemValue}
</ItemValueContainer>
</ItemStyleWrapper>
);
}
function handleItemCheckedStatus() {
updateIsChecked((previousState) => !previousState);
}
function handleItemValueChange(e) {
updateItemValue(e.target.value);
console.log(itemValue);
}
async function handleItemValueSave() {
updateIsSaving(true);
await updateInstance({
variables: {
actionId: UPDATE_ITEM_FOR_LIST_ACTION_ID,
executionParameters: JSON.stringify({
value: itemValue,
instanceId: item.id,
}),
},
refetchQueries,
});
updateIsSaving(false);
await handleEditModal();
console.log('Saving');
}
function handleCancelEdit() {
handleEditModal();
}
async function handleDelete() {
updateIsDeleting(true);
try {
await deleteInstance({
variables: {
actionId: DELETE_ITEM_FOR_LIST_ACTION_ID,
executionParameters: JSON.stringify({
parentInstanceId: parentId,
instanceId: item.id,
}),
},
refetchQueries,
});
await handleDeleteModal();
} catch (e) {
updateIsDeleting(false);
}
console.log('Deleting');
}
function handleCancelDelete() {
handleDeleteModal();
}
return (
<ItemStyleWrapper selected={selected}>
<ItemCheckBox htmlFor={item.id}>
<input
type='checkbox'
id={item.id}
checked={isChecked}
onChange={handleItemCheckedStatus}
/>
</ItemCheckBox>
<ItemValueContainer>{itemValue}</ItemValueContainer>
<Button type='button' onClick={handleEditModal}>
<FaEdit />
</Button>
<Button type='button' onClick={handleDeleteModal}>
<FaTrashAlt />
</Button>
<ModalContainer status={editModal} exitHandler={handleEditModal}>
<div className='modal-body'>
<EditInstanceForm
id={item.id}
label='Item Value:'
value={itemValue}
onChange={handleItemValueChange}
onSave={handleItemValueSave}
onCancel={handleCancelEdit}
disabled={isSaving}
/>
</div>
</ModalContainer>
<ModalContainer status={deleteModal} exitHandler={handleDeleteModal}>
<div className='modal-body'>
<ItemValueContainer>{itemValue}</ItemValueContainer>
<DeleteInstanceMenu
onDelete={handleDelete}
onCancel={handleCancelDelete}
disabled={isDeleting}
/>
</div>
</ModalContainer>
</ItemStyleWrapper>
);
}
export default compose(
graphql(EXECUTE, { name: 'updateInstance' }),
graphql(EXECUTE, { name: 'deleteInstance' })
)(Item);
And here's the react-modal code:
import React from 'react';
import Modal from 'react-modal';
import './modals.css';
const ModalContainer = ({ children, status, exithandler }) => {
return (
<Modal isOpen={status} onRequestClose={exithandler}>
{children}
</Modal>
);
};
export default ModalContainer;

Related

How can i test add filters button when clicked it opens search filters popup but before the button appears there is loading spinner?

add filter button is appeared when loading spinner is completed(means there is dispatch trigger action called loadListings which set loading to true).
I'm using react testing library, how can i achieve this by using mockedAxios? or any idea.
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
Search.js
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useNavigate} from '#reach/router';
import Picture from '#/components/picture';
import Button from '#/components/button';
import Pagination from '#/components/pagination';
import Tag from '#/components/tag';
import Spinner from '#/components/basic-spinner';
import Footer from '#/components/footer/footer';
import {
getAppliedSelectFilters,
getListings,
getListingsTotal,
listingsLoading,
} from '#/selectors/search';
import {showPopup} from '#/redux/modules/app/actions';
import {
saveDraftFilters,
clearDraftFilters,
draftSelectFilter,
clearAppliedFilter,
clearAllAppliedFilters,
loadListings,
} from '#/redux/modules/search/actions';
import {getSelectFilters, getSelectFiltersGroups} from '#/selectors/search';
import SearchFilterPopup from '#/components/search-filter-popup';
const Search = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const appliedFilters = useSelector(getAppliedSelectFilters);
const hits = useSelector(getListings);
const hitsCount = useSelector(getListingsTotal);
const loading = useSelector(listingsLoading);
const hitsPerPage = 10;
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
const showFilters = () => {
dispatch(
showPopup({
content: (
<SearchFilterPopup
saveDraftFilters={saveDraftFilters}
clearDraftFilters={clearDraftFilters}
draftSelectFilter={draftSelectFilter}
loadListings={loadListings}
selectFiltersSelector={getSelectFilters}
selectFiltersGroupsSelector={getSelectFiltersGroups}
/>
),
showClose: false,
}),
);
};
const handleTagClicked = item => {
dispatch(clearAppliedFilter(item));
dispatch(loadListings());
};
// eslint-disable-next-line #typescript-eslint/no-unused-vars
const handleClearAllFilters = () => {
dispatch(clearAllAppliedFilters());
dispatch(loadListings());
};
const handleBusinessSelected = ({identity, urlName}) => () => {
navigate(`/listings/${urlName}/${identity}`);
};
const handlePaginationPageClick = data => {
const indexOfLastHit = (data.selected + 1) * hitsPerPage;
const pages = indexOfLastHit - hitsPerPage;
dispatch(loadListings(pages));
};
return (
<section className='np-flex np-w-full np-filter-container np-overflow-x-hidden np-max-w-1300px np-flex-col np-justify-between np-mx-auto np-min-h-screen'>
<div className='np-flex np-pd np-flex-col np-justify-center'>
{!loading ? (
<div className='np-w-full'>
<div className='np-w-full'>
<h3 className='np-font-primary np-result-places np-mb-3 np-pt-8 np-truncate'>
<span>{hitsCount === 0 ? hitsCount : `${hitsCount}+`}</span>{' '}
{`Place${hitsCount !== 1 ? 's' : ''}`} in Dar es Salaam
</h3>
<div className='np-mb-3 np-flex'>
<Button
variant='link'
className='np-font-semibold np-uppercase'
onClick={showFilters}
>
<span>Add Filters</span>
</Button>
</div>
<div className='np-overflow-x-auto np-flex np-w-full'>
{appliedFilters.map(item => (
<Tag
key={item.name}
item={item}
labelKey='label'
toBe='removed'
onClick={handleTagClicked}
className='np-Tag-width'
/>
))}
{appliedFilters.length ? (
<Button variant='link' onClick={handleClearAllFilters}>
Clear all filters
</Button>
) : null}
</div>
</div>
</div>
) : null}
<div className='np-flex np-flex-wrap np-justify-center'>
{loading ? (
<div className='np-pt-32 np-h-80vh np-w-full np-flex np-justify-center'>
<Spinner size='large' label='loading' color='primary' />
</div>
) : hitsCount > 0 ? (
hits.map(hit => (
<div
key={hit.id}
className='np-search-card'
onClick={handleBusinessSelected(hit)}
>
<Picture
height='2/3'
src={`${
process.env.IMAGE_SERVICE_URL
}/cover:entropy/340x226/${hit.photos.length > 0 &&
hit.photos[hit.photos.length - 1].name}`}
className='np-overflow-hidden np-rounded-sm np-cursor-pointer'
/>
<section className='np-py-2 np-leading-normal'>
<h4 className='np-truncate'>{hit.category}</h4>
<h3 className='np-font-primary np-font-medium np-cursor-pointer np-truncate np-capitalize'>
{hit.name}{' '}
</h3>
<h4 className='np-text np-text-gray np-truncate'>
<span className='icon location'></span> {hit.location}
</h4>
</section>
</div>
))
) : null}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
actions.js
export const clearDraftFilters = () => ({
type: types.CLEAR_DRAFT_FILTERS,
});
export const loadListings = payload => ({
type: types.LOAD_LISTINGS,
payload,
});
export const loadListingStart = () => ({
type: types.LOAD_LISTINGS_START,
});
export const loadListingSuccess = payload => ({
type: types.LOAD_LISTINGS_SUCCESS,
payload,
});
export const loadListingError = error => ({
type: types.LOAD_LISTINGS_ERROR,
payload: error,
error: true,
});
epics.js
export const loadListingsEpic = (action$, state$, {API}) =>
action$.pipe(
filter(action => action.type === types.LOAD_LISTINGS),
switchMap(action => {
const features = getAppliedPlaceFeatureFilters(state$.value).join(',');
const category = getAppliedPlaceCategoryFilters(state$.value).join(',');
const q = get(state$.value, 'search.q', '');
const page = action.payload ? action.payload : null;
const query = omitEmpty({q, category, features, page, limit: 12});
return defer(() => API.getListings(query)).pipe(
switchMap(response => {
const [error, result] = response;
if (error) {
return of(actions.loadListingError(error));
}
return of(actions.loadListingSuccess(result));
}),
startWith(actions.loadListingStart()),
);
}),
);
reducers.js
const initialState = {
selectFilters: [],
suggestions: {docs: [], total: 0, loading: false},
};
export default (state = initialState, action) => {
switch (action.type) {
case types.LOAD_LISTINGS_START:
return {
...state,
listings: {loading: true},
};
case types.LOAD_LISTINGS_SUCCESS:
return {
...state,
listings: {loading: false, ...action.payload},
};
case types.LOAD_LISTINGS_ERROR:
return {
...state,
listings: {loading: false, error: action.payload},
};
default:
return state;
}
};
Search.test.js
import React from 'react';
import {render, screen, fireEvent} from '#/utils/testUtils';
import {waitFor} from '#testing-library/react';
import mockedAxios from 'axios';
import Search from './Search';
import { async } from 'rxjs/internal/scheduler/async';
jest.mock('axios');
describe('Search', () => {
afterEach(() => {
jest.resetAllMocks();
});
test('should render search result filters popup when add filters button is clicked', async () =>{
render(<Search />);
await waitFor(() => fireEvent.click(screen.getByRole('button', {name: 'Add Filters'}))
)
})
});

Component not rerendering after axios Get (React)

I'm trying to render List of items of my DB using React.Context.
All my request work pretty well.
when i console log my states first I get an empty array and then array with the data that I need but my component is not updating. I have to go to another page an then go back to this page to get the data. I don't really understand why... here are my files..
ArticlesContext.js :
import React, { useState, createContext, useEffect } from 'react';
import axios from 'axios'
export const ArticlesContext = createContext();
export function ArticlesProvider(props) {
const [articles, setArticles] = useState([]);
const [user, setUser] =useState(0)
async function getArticles () {
await axios.get(`/api/publicItem`)
.then(res => {
setArticles(res.data);
})
}
useEffect( () => {
getArticles()
}, [user])
console.log(articles);
return (
<ArticlesContext.Provider value={[articles, setArticles]}>
{props.children}
</ArticlesContext.Provider>
);
}
Inventaire.js :
import React, { useContext, useEffect, useState } from 'react';
import './Inventaire.css';
import { ArticlesContext } from '../../../context/ArticlesContext';
import DeleteAlert from './Delete/Delete';
import Modify from './Modify/Modify';
import Filter from './Filter/Filter';
import axios from 'axios'
import Crud from '../../Elements/Articles/Crud/Crud';
import List from './List/List';
export default function Inventaire() {
const [articles, setArticles] = useContext(ArticlesContext);
const [filter, setFilter] = useState(articles)
console.log(articles);
//list for Inputs
const cat = articles.map(a => a.category.toLowerCase());
const categoryFilter = ([...new Set(cat)]);
const gender = articles.map(a => a.gender.toLowerCase());
const genderFilter = ([...new Set(gender)]);
//Event Listenner
//Uncheck All checkboxes
function UncheckAll() {
const el = document.querySelectorAll("input.checkboxFilter");
console.log(el);
for (var i = 0; i < el.length; i++) {
var check = el[i];
if (!check.disabled) {
check.checked = false;
}
}
}
//SearchBar
const searchChange = (e) => {
e.preventDefault();
const stuff = articles.filter((i) => {
return i.name.toLowerCase().match(e.target.value.toLowerCase())
})
setFilter(stuff)
UncheckAll(true)
}
const Types = (e) => {
if (e.target.checked === true) {
const stuff = filter.filter((i) => {
return i.category.toLowerCase().match(e.target.value.toLowerCase())
})
setFilter(stuff)
console.log(articles);
} else if (e.target.checked === false) {
setFilter(articles)
}
}
const Gender = (e) => {
if (e.target.checked === true) {
const stuff = filter.filter((i) => {
console.log(i.category, e.target.value);
return i.gender.toLowerCase().match(e.target.value.toLowerCase())
})
setFilter(stuff)
} else if (e.target.checked === false) {
setFilter(articles)
}
}
return (
<div className="inventaireContainer">
<input type="text" placeholder="Recherche un Article" onChange={searchChange} />
<div className="inventaireMenu">
<Crud />
<Filter
filter={Types}
categorys={categoryFilter}
genre={genderFilter}
target={Gender}
/>
</div>
<List filter={filter} articles={articles}/>
</div>
)
}
List.js :
import React from 'react';
import DeleteAlert from '../Delete/Delete';
import Modify from '../Modify/Modify';
export default function List({ filter, articles }) {
return (
<div>
{filter.map((details, i) => {
return (
<div className="inventaireBlock" >
<div className="inventaireGrid">
<div className="inventaireItemImg">
<img src={details.image} alt="ItemImg" />
</div>
<h2>{details.name}</h2>
<h3>{details.category}</h3>
<h3>{details.gender}</h3>
<div>
<p>S :{details.sizes[0].s}</p>
<p>M :{details.sizes[0].m}</p>
<p>L :{details.sizes[0].l}</p>
<p>XL :{details.sizes[0].xl}</p>
</div>
<h2> Prix: {details.price}</h2>
<div className="modify">
<Modify details={details._id} />
</div>
<div className="delete" >
<DeleteAlert details={details._id} articles={articles} />
</div>
</div>
</div>
)
})}
</div>
)
}
Thanks for your time

Having React Context in Separate File, Can't Get Component to Not re-render

I've got a simple example of React Context that uses useMemo to memoize a function and all child components re-render when any are clicked. I've tried several alternatives (commented out) and none work. Please see code at stackblitz and below.
https://stackblitz.com/edit/react-yo4eth
Index.js
import React from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import { GlobalProvider } from "./GlobalState";
function App() {
return (
<GlobalProvider>
<Hello />
</GlobalProvider>
);
}
render(<App />, document.getElementById("root"));
GlobalState.js
import React, {
createContext,useState,useCallback,useMemo
} from "react";
export const GlobalContext = createContext({});
export const GlobalProvider = ({ children }) => {
const [speakerList, setSpeakerList] = useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
]);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakerList((currentState) => {
return currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
});
});
},[]);
// const provider = useMemo(() => {
// return { clickFunction: clickFunction, speakerList: speakerList };
// }, []);
//const provider = { clickFunction: clickFunction, speakerList: speakerList };
const provider = {
clickFunction: useMemo(() => clickFunction,[]),
speakerList: speakerList,
};
return (
<GlobalContext.Provider value={provider}>{children}</GlobalContext.Provider>
);
};
Hello.js
import React, {useContext} from "react";
import Speaker from "./Speaker";
import { GlobalContext } from './GlobalState';
export default () => {
const { speakerList } = useContext(GlobalContext);
return (
<div>
{speakerList.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</div>
);
};
Speaker.js
import React, { useContext } from "react";
import { GlobalContext } from "./GlobalState";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</>
);
});
Couple of problems in your code:
You already have memoized the clickFunction with useCallback, no need to use useMemo hook.
You are consuming the Context in Speaker component. That is what's causing the re-render of all the instances of Speaker component.
Solution:
Since you don't want to pass clickFunction as a prop from Hello component to Speaker component and want to access clickFunction directly in Speaker component, you can create a separate Context for clickFunction.
This will work because extracting clickFunction in a separate Context will allow Speaker component to not consume GlobalContext. When any button is clicked, GlobalContext will be updated, leading to the re-render of all the components consuming the GlobalContext. Since, Speaker component is consuming a separate context that is not updated, it will prevent all instances of Speaker component from re-rendering when any button is clicked.
Demo
const GlobalContext = React.createContext({});
const GlobalProvider = ({ children }) => {
const [speakerList, setSpeakerList] = React.useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true }
]);
return (
<GlobalContext.Provider value={{ speakerList, setSpeakerList }}>
{children}
</GlobalContext.Provider>
);
};
const ClickFuncContext = React.createContext();
const ClickFuncProvider = ({ children }) => {
const { speakerList, setSpeakerList } = React.useContext(GlobalContext);
const clickFunction = React.useCallback(speakerIdClicked => {
setSpeakerList(currentState => {
return currentState.map(rec => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
});
});
}, []);
return (
<ClickFuncContext.Provider value={clickFunction}>
{children}
</ClickFuncContext.Provider>
);
};
const Speaker = React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const clickFunction = React.useContext(ClickFuncContext)
return (
<div>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</div>
);
});
function SpeakerList() {
const { speakerList } = React.useContext(GlobalContext);
return (
<div>
{speakerList.map(rec => {
return (
<Speaker speaker={rec} key={rec.id} />
);
})}
</div>
);
};
function App() {
return (
<GlobalProvider>
<ClickFuncProvider>
<SpeakerList />
</ClickFuncProvider>
</GlobalProvider>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can also see this demo on StackBlitz
this will not work if you access clickFuntion in children from provider because every time you updating state, provider Object will be recreated and if you wrap this object in useMemolike this:
const provider = useMemo(()=>({
clickFunction,
speakerList,
}),[speakerList])
it will be recreated each time clickFunction is fired.
instead you need to pass it as prop to the children like this:
import React, {useContext} from "react";
import Speaker from "./Speaker";
import { GlobalContext } from './GlobalState';
export default () => {
const { speakerList,clickFunction } = useContext(GlobalContext);
return (
<div>
{speakerList.map((rec) => {
return <Speaker speaker={rec} key={rec.id} clickFunction={clickFunction }></Speaker>;
})}
</div>
);
};
and for provider object no need to add useMemo to the function clickFunction it's already wrapped in useCallback equivalent to useMemo(()=>fn,[]):
const provider = {
clickFunction,
speakerList,
}
and for speaker component you don't need global context :
import React from "react";
export default React.memo(({ speaker,clickFunction }) => {
console.log("render")
return (
<>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</>
);
});

Redux loses state when navigating to another page in Next.js

I'm creating the redux state in this page :
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import wrapper from '../redux/store';
import Container from '../components/Container/Container';
import Card from '../components/Card/Card';
import Circle from '../components/Circle/Circle';
import PieChart from '../components/PieChart/PieChart';
import Accordion from '../components/Accordion/Accordion';
import RadioButton from '../components/Ui/RadioButton/RadioButton';
import { manageList, reportList } from '../components/helper';
import { getManageListAndCategoryId } from '../redux/actions/actions';
const Panel = ({ manageProductsList }) => (
<>
{console.log(manageProductsList)}
<MainContainer>
<Title>Управление</Title>
<ContainersWrapper>
{manageProductsList.map((item, index) => <Card key={index} title={item.title} type="service" serviceName={item.value} />)}
</ContainersWrapper>
<SecondSection>
<CustomContainer>
<Title>Отчетность</Title>
<p>Показатели за:</p>
Здесь будут ТАБЫ
<ContainersWrapper>
{reportList.map((item, index) => <Card key={index} item={item} type="report" />)}
</ContainersWrapper>
<DiagreammWrapper>
<PieChart />
<Circle percent={20} />
<Circle percent={87} />
<Circle percent={30} />
<Circle percent={47} />
</DiagreammWrapper>
</CustomContainer>
</SecondSection>
<CustomContainer>
<TitleTwo>Доступные отчеты</TitleTwo>
<Accordion />
<RadioButton />
</CustomContainer>
</MainContainer>
</>
);
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getManageListAndCategoryId(manageList));
});
const mapStateToProps = (state) => ({
manageProductsList: state.mainReducer.manageProductsList,
});
export default connect(mapStateToProps, null)(Panel);
And I still can see the data manageProductsList (screenshot) in Redux in this page. But when I navigate to another dynamic route page forms/[id.tsx]
import React from 'react';
import { connect } from 'react-redux';
import wrapper from '../redux/store';
import { util, manageList, reportList } from '../../components/helper';
import { getManageListAndCategoryId } from '../../redux/actions/actions';
export async function getStaticPaths(categoryIds) {
console.log('categoryIds', categoryIds);
//temporarely make static path data while categoryIds is undefined
const paths = [
{ params: { id: 'object' } },
{ params: { id: 'service' } },
{ params: { id: 'club_cards' } },
{ params: { id: 'schedule' } },
{ params: { id: 'agents' } },
{ params: { id: 'abonements' } },
{ params: { id: 'price_category' } },
{ params: { id: 'person_data' } },
{ params: { id: 'roles' } },
];
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ params, manageProductsList }) {
// const postData = util.findFormData(params.id, manageProductsList);
const postData = { title: 'asdsadasdsad' };
return {
props: {
postData,
},
};
}
const Form = ({ manageProductsList }) => (
<div>
{console.log(manageProductsList)}
{/* {postData.title} */}
dasdsadsad
</div>
);
const mapStateToProps = (state) => ({
categoryIds: state.mainReducer.categoryIds,
manageProductsList: state.mainReducer.manageProductsList,
});
export default connect(mapStateToProps, null)(Form);
the manageProductsList and categoryIds are empty arrays (screenshot 2)
I am using native Link from next/link component to navigate the page
Here is Card component which navigate to dynamic page:
import React, { FunctionComponent, HTMLAttributes } from 'react';
import styled from 'styled-components';
import Link from 'next/link';
import EditIcon from '#material-ui/icons/Edit';
import AddIcon from '#material-ui/icons/Add';
interface CardProps extends HTMLAttributes<HTMLOrSVGElement>{
title: string
type: string
item?: {
title: string
amount: number
}
serviceName: string
}
const Card: FunctionComponent<CardProps> = ({
type, title, serviceName, item,
}) => (
<>
{
type === 'service'
&& (
<FirstSection>
<h1>{title}</h1>
<ImageWrapper>
<Link href={`/forms/${serviceName}`}>
<a><AddIcon fontSize="large" onClick={(e) => { console.log(serviceName); }} /></a>
</Link>
<EditIcon />
</ImageWrapper>
</FirstSection>
)
}
{
type === 'report'
&& (
<SecondSection>
<h1>{item.title}</h1>
<p>{item.amount}</p>
</SecondSection>
)
}
</>
);
export default Card;
I would be very gratefull if someone can help
Your <Link> will cause server-side rendering, you can observe whether the browser tab is loading or not when navigate to another page. If it is, the page will reload and the redux state would be refresh.
The official docs shows the right way for using dynamic route.
<Link href="/forms/[id]" as={`/forms/${serviceName}`}>

Testing Apollo Query with React Hook Client

I am trying to write test for this component using jest
import { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Query } from 'react-apollo';
import { updateYourDetails } from 'universal/domain/health/yourDetails/yourDetailsActions';
import Input from 'universal/components/input/input';
import InputNumber from 'universal/components/input/inputNumber/inputNumber';
import AsyncButton from 'universal/components/asyncButton/asyncButton';
import ErrorMessage from 'universal/components/errorMessage/errorMessage';
import Link from 'universal/components/link/link';
import analytics from 'universal/utils/analytics/analytics';
import { isChatAvailable } from 'universal/logic/chatLogic';
import { validators } from 'universal/utils/validation';
import { localTimezone, getWeekdays } from 'universal/utils/date';
import {
CALL_ME_BACK_LOADING_MSG,
CALL_ME_BACK_LABELS_SCHEDULE_TIME,
CALL_ME_BACK_LABELS_SELECTED_DATE,
CALL_ME_BACK_ERROR_MSG,
CALL_ME_BACK_TEST_PARENT_WEEKDAY,
CALL_ME_BACK_TEST_CHILD_WEEKDAY,
} from 'universal/constants/callMeBack';
import CallCenterAvailibility from './CallCenterAvailibility';
import SelectWrapper from './SelectWrapper';
import SelectOption from './SelectOption';
import styles from './callMeBackLightBox.css';
import { CALL_ME_BACK_QUERY } from './callMeBackQuery';
import postData from './postData';
export const CallMeForm = props => {
const initSelectedDate = getWeekdays()
.splice(0, 1)
.reduce(acc => ({ ...acc }));
const { onSubmissionComplete, className, variant } = props;
const [hasSuccessfullySubmitted, setHasSuccessfullySubmitted] = useState(false);
const [apiStatus, setApiStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [cellNumber, setCallNumber] = useState(props.cellNumber || '');
const [customerFirstName, setCustomerFirstName] = useState(props.customerFirstName || '');
const [number, setNumber] = useState(props.Number || '');
const [selectedDate, setSelectedDate] = useState(initSelectedDate || '');
const [scheduledTime, setScheduledTime] = useState('');
const weekdays = getWeekdays() || [];
const timezone = localTimezone || '';
const requestReceived = apiStatus === 'CALLBACK_ALREADY_EXIST';
const cellNumberInput = useRef(null);
const customerFirstNameInput = useRef(null);
const getQuery = () => (
<Query query={CALL_ME_BACK_QUERY} variables={{ weekday: selectedDate.weekday }}>
{({ data, error, loading }) => {
if (loading)
return (
<SelectWrapper disabled labelTitle={CALL_ME_BACK_LABELS_SCHEDULE_TIME} name="scheduledTime">
<SelectOption label={CALL_ME_BACK_LOADING_MSG} />
</SelectWrapper>
);
if (error) return <ErrorMessage hasError errorMessage={<p>{CALL_ME_BACK_ERROR_MSG}</p>} />;
return (
<CallCenterAvailibility
selectedDate={selectedDate}
callCenterBusinessHour={data.callCenterBusinessHour}
onChange={val => setScheduledTime(val)}
/>
);
}}
</Query>
);
const getPostSubmitMessage = (firstName: string, type: string) => {
const messages = {
callCentreClosed: `a`,
requestReceived: `b`,
default: `c`,
};
return `Thanks ${firstName}, ${messages[type] || messages.default}`;
};
const validate = () => {
const inputs = [customerFirstNameInput, cellNumberInput];
const firstInvalidIndex = inputs.map(input => input.current.validate()).indexOf(false);
const isValid = firstInvalidIndex === -1;
return isValid;
};
const onSubmitForm = event => {
event.preventDefault();
onSubmit();
};
const onSubmit = async () => {
if (variant === '0' && !validate()) {
return;
}
analytics.track(analytics.events.callMeBack.callMeBackSubmit, {
trackingSource: 'Call Me Form',
});
setIsLoading(true);
const srDescription = '';
const response = await postData({
cellNumber,
customerFirstName,
number,
scheduledTime,
timezone,
srDescription,
});
const { status } = response;
const updatedSubmissionFlag = status === 'CALLBACK_ALREADY_EXIST' || status === 'CALLBACK_ADDED_SUCCESSFULLY';
// NOTE: add a slight delay for better UX
setTimeout(() => {
setApiStatus(apiStatus);
setIsLoading(false);
setHasSuccessfullySubmitted(updatedSubmissionFlag);
}, 400);
// Update Redux store
updateYourDetails({
mobile: cellNumber,
firstName: customerFirstName,
});
if (onSubmissionComplete) {
onSubmissionComplete();
}
};
if (hasSuccessfullySubmitted) {
return (
<p aria-live="polite" role="status">
{getPostSubmitMessage(
customerFirstName,
(!requestReceived && !isChatAvailable() && 'callCentreClosed') || (requestReceived && 'requestReceived')
)}
</p>
);
}
return (
<form onSubmit={onSubmitForm} className={className}>
{variant !== '1' && (
<>
<label htmlFor="customerFirstName" className={styles.inputLabel}>
First name
</label>
<Input
className={styles.input}
initialValue={customerFirstName}
isMandatory
maxLength={20}
name="customerFirstName"
onChange={val => setCustomerFirstName(val)}
ref={customerFirstNameInput}
value={customerFirstName}
{...validators.plainCharacters}
/>
</>
)}
{variant !== '1' && (
<>
<label htmlFor="cellNumber" className={styles.inputLabel}>
Mobile number
</label>
<Input
className={styles.input}
initialValue={cellNumber}
isMandatory
maxLength={10}
name="cellNumber"
onChange={val => setCallNumber(val)}
ref={cellNumberInput}
type="tel"
value={cellNumber}
{...validators.tel}
/>
</>
)}
{variant !== '1' && (
<>
{' '}
<label htmlFor="number" className={styles.inputLabel}>
Qantas Frequent Flyer number (optional)
</label>
<InputNumber
className={styles.input}
disabled={Boolean(props.number)}
initialValue={number}
name="number"
onChange={val => setNumber(val)}
value={number}
/>
</>
)}
{weekdays && (
<>
<SelectWrapper
testId={`${CALL_ME_BACK_TEST_PARENT_WEEKDAY}`}
labelTitle={CALL_ME_BACK_LABELS_SELECTED_DATE}
name="selectedDate"
onChange={val =>
setSelectedDate({
...weekdays.filter(({ value }) => value === val).reduce(acc => ({ ...acc })),
})
}
tabIndex={0}
>
{weekdays.map(({ value, label }, i) => (
<SelectOption
testId={`${CALL_ME_BACK_TEST_CHILD_WEEKDAY}-${i}`}
key={value}
label={label}
value={value}
/>
))}
</SelectWrapper>
{getQuery()}
</>
)}
<AsyncButton className={styles.submitButton} onClick={onSubmit} isLoading={isLoading}>
Call me
</AsyncButton>
<ErrorMessage
hasError={(apiStatus >= 400 && apiStatus < 600) || apiStatus === 'Failed to fetch'}
errorMessage={
<p>
There was an error submitting your request to call you back. Please try again or call us at{' '}
<Link href="tel:134960">13 49 60</Link>.
</p>
}
/>
</form>
);
};
CallMeForm.propTypes = {
cellNumber: PropTypes.string,
customerFirstName: PropTypes.string,
number: PropTypes.string,
onSubmissionComplete: PropTypes.func,
className: PropTypes.string,
variant: PropTypes.string,
};
const mapStateToProps = state => {
const { frequentFlyer, yourDetails } = state;
return {
cellNumber: yourDetails.mobile,
customerFirstName: yourDetails.firstName,
number: frequentFlyer.memberNumber,
};
};
export default connect(mapStateToProps)(CallMeForm);
My test file is as below
import { render, cleanup } from '#testing-library/react';
import { MockedProvider } from 'react-apollo/test-utils';
import { shallow } from 'enzyme';
import MockDate from 'mockdate';
import { isChatAvailable } from 'universal/logic/chatLogic';
import { CALL_ME_BACK_QUERY } from './callMeBackQuery';
import { CallMeForm } from './CallMeForm';
import postData from './postData';
jest.mock('universal/components/input/input', () => 'Input');
jest.mock('universal/components/asyncButton/asyncButton', () => 'AsyncButton');
jest.mock('universal/components/errorMessage/errorMessage', () => 'ErrorMessage');
jest.mock('universal/logic/chatLogic');
jest.mock('./postData');
describe('CallMeForm', () => {
let output;
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
const mockQueryData = [
{
client:{},
request: {
query: CALL_ME_BACK_QUERY,
variables: { weekday: '' },
},
result: {
data: {
callCenterBusinessHour: {
timeStartHour: 9,
timeStartMinute: 0,
timeEndHour: 5,
timeEndMinute: 0,
closed: false,
},
},
},
},
];
const { container } = render(<MockedProvider mocks={mockQueryData} addTypename={false}><CallMeForm /></MockedProvider>);
output = container;
});
afterEach(cleanup);
it('renders correctly', () => {
expect(output).toMatchSnapshot();
});
});
I keep getting error: TypeError: this.state.client.stop is not a function
I also removed <MockedProvider> wrapper and I got another error Invariant Violation: Could not find "client" in the context or passed in as a prop. Wrap the root component in an , or pass an ApolloClient instance in
via props.
Does anyone know why I get this error and how to fix this?
I have not the solution, but I've got some information.
First of all, I'm having the same error here, rendering with #testing-library/react.
I then tried to render with ReactDOM, like that:
// inside the it() call with async function
const container = document.createElement("div");
ReactDOM.render(
< MockedProvider {...props}>
<MyComponent />
</MockedProvider>,
container
);
await wait(0);
expect(container).toMatchSnapshot();
And also tried to render with Enzyme, like that:
// inside the it() call, with async function too
const wrapper = mount(
<MockedProvider {...props}>
<MyComponent />
</MemoryRouter>
);
await wait(0);
expect(wrapper.html()).toMatchSnapshot();
Both ReactDOM and Enzyme approaches worked fine.
About the error we're getting, I think maybe it's something related with #testing-library/react =/
I didn't tried to render with react-test-renderer, maybe it works too.
Well, that's what I get... maybe it helps you somehow.
Ps.: About waait: https://www.apollographql.com/docs/react/development-testing/testing/#testing-final-state
EDIT 5 Feb 2020:
Based on https://github.com/apollographql/react-apollo/pull/2165#issuecomment-478865830, I found that solution (it looks ugly but works ¯\_(ツ)_/¯):
<MockedProvider {...props}>
<ApolloConsumer>
{client => {
client.stop = jest.fn();
return <MyComponent />;
}}
</ApolloConsumer>
</MockedProvider>
I had the same problem and was able to solve it. I had a missing peer dependency.
Your package.json is not shown so I am not sure if your problem is the same as mine but I was able to resolve the problem by installing "apollo-client".
I am using AWS Appsync for my client and hence did not have apollo-client installed.

Resources