I have this really simple React component:
import { useState } from "react";
import FullCalendar from "#fullcalendar/react";
import dayGridPlugin from "#fullcalendar/daygrid";
import { getCalendarEvents } from "../../utils/frontend/getEvents";
import EventModal from "../CreateEventModal";
const Calendar = ({ calendar: calendarId, eventColor }) => {
const [isModalVisible, setModalVisibility] = useState(false);
return (
<>
<button onClick={() => setModalVisibility(true)}>Open Modal</button>
<FullCalendar
plugins={[dayGridPlugin]}
events={getCalendarEvents(calendarId)}
/>
<EventModal open={isModalVisible} onClose={() => setModalVisibility(false)} />
</>
);
}
export default Calendar;
Yet, there is a problem. When ever I open the modal, Fullcalendar decides to re-fetch my events. Here the code for getCalendarEvents:
export const getCalendarEvents =
(calendarId) => (info, successCallback, failureCallback) => {
fetch(`/list-events?id=${calendarId}&timeMin=${info.start}&timeMax=${info.end}`)
.then((res) => res.json())
.then((res) => {
if (res.success && res.data)
successCallback(
res?.data?.map((event) => ({
id: event.id,
title: event.summary,
location: event.location,
description: event.description,
start: event.start?.dateTime,
end: event.end?.dateTime,
})) || []
)
else {
failureCallback(new Error(res.error || "Something went wrong."));
}
})
};
I believe this is an issue with Fullcalendar itself. However, is there are way to fix this issue from my side?
I am using #fullcalendar/react version 5.10.1.
I believe this happens because every render is processes "events={getCalendarEvents(calendarId)}", which creates new reference to function.
Try something like that:
import { useState, useMemo } from "react";
import FullCalendar from "#fullcalendar/react";
import dayGridPlugin from "#fullcalendar/daygrid";
import { getCalendarEvents } from "../../utils/frontend/getEvents";
import EventModal from "../CreateEventModal";
const Calendar = ({ calendar: calendarId, eventColor }) => {
const [isModalVisible, setModalVisibility] = useState(false);
const memoizedEventsFn = useMemo(() => {
return getCalendarEvents(calendarId);
}, [calendarId]);
return (
<>
<button onClick={() => setModalVisibility(true)}>Open Modal</button>
<FullCalendar
plugins={[dayGridPlugin]}
events={memoizedEventsFn}
/>
<EventModal open={isModalVisible} onClose={() => setModalVisibility(false)} />
</>
);
}
export default Calendar;
I hope it works. If not, it is possible to memoize FullCalendar with memo or useMemo.
Related
I am trying to create a button that I can put anywhere on a page to submit a specific form. I'm interested in using Hooks and Context / Provider pattern to expose this state anywhere in the app.
The code below creates a context / provider that has access to a useState hook for setting the submit function.
import React from 'react';
import {createContext, ReactNode, useContext, useEffect, useRef, useState} from 'react';
export type HiddenButtonSubmitContext = {
submit: () => void,
setSubmitHandler: React.Dispatch<React.SetStateAction<() => void>>
}
export const defaultSetSubmitHandler = () => () => {
// eslint-disable-next-line no-console
console.log('submit handler was never set')
}
export const SubmitContext = createContext<HiddenButtonSubmitContext>({
submit: () => {
throw new Error('submit not set, make surea all calls to useHiddenButton are within HiddenButtonProvider');
},
setSubmitHandler: defaultSetSubmitHandler()
})
export const useHiddenButton = () => useContext(SubmitContext)
export const HiddenButtonProvider: React.FC<{ children?: ReactNode | undefined }> = ({ children }) => {
const [submit, setSubmitHandler] = useState(defaultSetSubmitHandler)
return (
<SubmitContext.Provider value={{submit, setSubmitHandler}}>
{children}
</SubmitContext.Provider>
)
}
export const HiddenButton = () => {
const hiddenButtonRef = useRef<any>()
const { setSubmitHandler } = useHiddenButton()
useEffect(() => {
setSubmitHandler(() => () => {
hiddenButtonRef.current.click()
})
}, [setSubmitHandler])
return (
<input type="submit" style={{ display: 'none' }} ref={hiddenButtonRef} />
)
}
Here's the example usage:
const Btn = () => {
const { submit } = useHiddenButton()
return <button onClick={() => submit()}>hi</button>
}
export function Example () {
return (
<HiddenButtonProvider>
<Btn />
<form onSubmit={(e) => {
e.preventDefault()
console.log('submitted')
}}>
<HiddenButton/>
</form>
</HiddenButtonProvider>
)
}
Clunking through learning testing with jest + enzyme. I have an array, OptionsArray, with some options that get mapped to buttons in a component. I figured that in the testing suite for the component, I could just do
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { OptionsArray } from './ConfigOptions';
import Foo from './Foo';
describe('Foo', () => {
let wrapper: ShallowWrapper;
const numberOfOptions = OptionsArray.length;
beforeEach(() => (wrapper = shallow(<Foo />)));
it('renders exactly one Button Item for each option', () => {
/* eslint-disable-next-line testing-library/no-debugging-utils */
console.log(wrapper.debug());
OptionsArray.forEach((option) => {
console.log(option.value);
});
OptionsArray.forEach((option) => {
expect(wrapper.find(option.value)).toHaveLength(1);
});
});
});
I see the options fine in the console output, but then I get:
Foo › renders exactly one Button Item for each option
expect(received).toHaveLength(expected)
Expected length: 1
Received length: 0
So I'm guessing that I'm passing the variable to find incorrectly? Is there a better way to do this?
Adding component Foo:
/* Foo.tsx */
import React, { useState } from 'react';
import { Button, ListGroup } from 'react-bootstrap';
import { OptionsArray } from './ConfigOptions';
import './Foo.scss';
const Foo: React.FC<> = () => {
const [options, setOptions] = useState(OptionsArray);
return (
<div className="Foo">
<ListGroup>
{OptionsArray.map((option, i) => (
<ListGroup.Item key={i}>
<Button
id={i.toString()}
value={option.value}
onClick={(e) => handleClick(e.currentTarget.id)}
variant={option.isSet ? 'primary' : 'outline-primary'}
>
{option.value}
</Button>
{option.content}
</ListGroup.Item>
))}
</ListGroup>
</div>
);
};
export default Foo;
And the OptionsArray:
import React from 'react';
export const OptionsArray = [
{
value: 'OptionA',
content: (
<React.Fragment>
<br />
<p>Here is a description of OptionA.</p>
</React.Fragment>
),
isSet: false,
},
{
value: 'OptionB',
content: (
<React.Fragment>
<br />
<p>Here is a description of OptionB.</p>
</React.Fragment>
),
isSet: false,
},
];
I figured it out. As usual, just a misunderstanding on my part. I was trying to use find to get the Button components by text, but this isn't how find works. Instead, I needed to use the findWhere method and a predicate to hunt down the exact components I was looking for. Here was my solution:
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { OptionsArray } from './ConfigOptions';
import Foo from './Foo';
describe('Foo', () => {
let wrapper: ShallowWrapper;
const numberOfOptions = OptionsArray.length;
beforeEach(() => (wrapper = shallow(<Foo />)));
it('renders exactly one Button Item for each option', () => {
OptionsArray.forEach((option) => {
expect(wrapper.find({ value: option.value })).toHaveLength(1);
const ButtonWithText = wrapper.findWhere((node) => {
return node.name() === 'Button' && node.text() === option.value;
});
expect(ButtonWithText ).toHaveLength(1);
});
});
});
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;
I have the following code which I use to subscribe to branch.io. Here, the first console.log() is printed. But it doesn't go beyond that. What am I doing wrong here?
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import branch from 'react-native-branch';
import {
Button,
InviteSummary,
ListItem,
ScreenContainer,
Seperator,
Label,
} from 'components';
import { APP_HOME_SCREEN } from 'common/constants/screens';
import { copyContent, shareContent } from 'library/Utils';
import ChevronRight from 'res/icons/Chevron/Right';
import Copied from 'res/icons/System/checked.svg';
import Copy from 'res/icons/System/Copy';
const SCREEN_TITLE = 'Invite and earn';
let _unsubscribeFromBranch = null;
const Invite = () => {
const [referralLink, setReferralLink] = useState('');
const [linkCopied, handleCopy] = useState(false);
useEffect(() => {
console.log('before subscribe');
_unsubscribeFromBranch = branch.subscribe(({ error, params }) => {
console.log('in subscribe');
if (error) {
console.error('Error from Branch: ', error);
return;
}
console.log('Branch params: ', JSON.stringify(params));
if (!params['+clicked_branch_link']) return;
});
}, []);
const handleCopyLink = (text: string) => copyContent(text, () => handleCopy(true));
const handleShareLink = () => shareContent(referralLink);
return (
<>
<ScreenContainer
title={SCREEN_TITLE}
backScreen={APP_HOME_SCREEN}
backScreenIcon={<ChevronRight />}>
<View>
<InviteSummary
upperMainText='S$40.00'
lowerMainText='earned'
leftSubText='7 invites earned'
rightSubText='3 pending'
/>
<Label
type={Label.Types.BODY_SMALL}
label='when you invite a friend to no matter how many friends! To make things sweeter, each friend you invite gets S$5 too.'
/>
<Label
type={Label.Types.BODY_SMALL}
label='Referral credit will be awarded to you automatically once your friend activates their Visa debit card.'
/>
<Seperator />
<Label
type={Label.Types.BODY_SMALL}
label='SHARE Your unique invite link'
/>
<ListItem
title={referralLink}
rightIcon={linkCopied ? <Copied /> : <Copy />}
disabled={linkCopied}
onPress={() => handleCopyLink(referralLink)}
/>
</View>
</ScreenContainer>
<View>
<Button
type={Button.Types.PRIMARY}
text='SHARE LINK'
disabled={!linkCopied}
onPress={handleShareLink}
/>
</View>
</>
);
};
export default Invite;
I have 2 collections on firestore, boxes contains a field shoesthat is an array of reference id to the shoes collections:
My Boxes component is fetching all boxes and displaying their number. I also want to display properties of the shoes in it, like a photo. So I go about like that:
Boxes.jsx
// DEPENDENCIES
import React, { useEffect, useContext } from 'react';
// COMPONENTS
import BoxCard from '../Components/BoxCard';
// CONTEXT
import ShoesContext from '../Contexts/ShoesContext';
// HELPERS
import db from '../config/firebase';
let initialBoxes = [];
const Boxes = () => {
const { boxes, setBoxes } = useContext(ShoesContext);
useEffect(() => {
initialBoxes = [];
db.collection('boxes')
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
initialBoxes.push(doc);
});
setBoxes(initialBoxes);
});
}, []);
return (
<div>
<h3>You have {boxes.length} boxes:</h3>
{!boxes ? (
<div>Loading..</div>
) : (
boxes.map(box => {
return <BoxCard box={box} key={box.id} />;
})
)}
</div>
);
};
export default Boxes;
Boxes.jsx
import React from 'react';
import TestComponent from './TestComponent';
const BoxCard = ({ box }) => {
const theBox = box.data();
return (
<div>
<h5>
Box number {theBox.number} has {theBox.shoes.length} shoes:{' '}
</h5>
{theBox.shoes.map(shoe => {
return <TestComponent shoe={shoe} />;
})}
</div>
);
};
export default BoxCard;
and this is where it all breaks:
import React from 'react';
const TestComponent = ({ shoe }) => {
useEffect(() => {
let hell;
shoe.get().then(doc => {
hell = doc.data();
});
}, []);
return <div>{hell ? hell.season : 'loading...'}</div>;
};
export default TestComponent;
hell is undefined. I have not found a way to render the nested docs so I can use them in my TestComponent component. My extensive research online has not been succesful so far, hence my question today.
Thanks!
Update:
I seem to have found the answer, answer from Josh below put me on the right track. See below code for TestComponent.jsx:
import React, { useEffect, useState } from 'react';
// HELPERS
import db from '../config/firebase';
const TestComponent = ({ shoe }) => {
const [hell, setHell] = useState();
useEffect(() => {
db.collection('shoes')
.doc(shoe.id)
.get()
.then(doc => {
setHell(doc.data());
});
}, []);
return <div>{hell ? hell.season : 'loading...'}</div>;
};
export default TestComponent;
What is shoe in shoe.get()... in the TestComponent? Shouldn't it be db.doc(shoe).get()....