const csvLinkRef = useRef(null)
const exportFile = () => {
let data= {data:'info'}
//Async Action
dispatch(
getFileData(data,
()=>{
csvLinkRef.current.link.click()
}
))
}
<Button onClick={exportFile}> //run exportFile on click
<CSVLink
filename={'file.csv'}
data={someData}
ref={csvLinkRef}//set ref here
>
Export
</CSVLink>
</Button>
//Async API call using redux thunk
export const getFileData = (data,cb) => async dispatch => {
try{
const res = await callApi('ever.php',data,'POST')// make api call
if(res.status==="00"){
dispatch({
type:GET_ALL_DATA,
data: res.data,
})
}
}catch(error){
console.log(error)
}finally{
cb()
}
}
My issue is csvLinkRef.current.link.click() fires non-stop after the API call. How can I make it fire just once? Is there a way I can 'unset' the ref? Please help.(I'm using react-csv library).
The getFileData action API call is successful and the data is in redux state.
if csvLinkRef.current.link.click fired only once, everything would be fine
I had the same problem. Now it is solved. You can try it...
function CsvExample({ manufacturerList }: IdLinkCreateProps) {
const httpRequests = new HttpRequests()
const csvInstance = useRef<any | null>(null);
const [alertOpt, setAlertOpt] = useState({ isOpen: false, message: '', variant: '' })
const [manufacturer, setManufacturer] = useState<IManufacturerItem | null>(null)
const [quantity, setQuantity] = useState<number>(0)
const [idList, setIdList] = useState<{ idList: string }[]>([])
const header = [{ label: "Digital-Links", key: "idLink" }];
const createIdLink = async () => {
let data: IIdLinkCreate = { manufacturer: manufacturer!, quantity: quantity! }
try {
const result = await httpRequests.idLinkCreate(data)
setSuccessOptions(result)
} catch (error) {
console.log('error on getting trusted manufacturer list: ', error);
}
return
}
const setSuccessOptions = (result: IBackendResult) => {
if (result.success) {
setIdList(result.data);
} else {
setAlertOpt({ isOpen: true, message: 'FAILED', variant: 'danger' })
console.log('error on updating data: ', result.error);
}
}
useEffect(() => {
if (idList && idList.length > 0 && csvInstance?.current?.link) {
csvInstance.current.link.click();
}
}, [idList]);
return (
<div className='id_link_wrapper'>
<AppHeader />
<Alert className="alert_error" variant={alertOpt.variant} show={alertOpt.isOpen} >
{alertOpt.message}
</Alert>
<IdLinkTitlePart />
<IdLinkInputPart
manufacturerList={manufacturerList}
setManufacturer={setManufacturer}
setQuantity={setQuantity}
quantity={quantity}
/>
<Button
onClick={createIdLink}
className='button_element'> CSV EXPORTIEREN
</Button>
<CSVLink
asyncOnClick={true}
headers={header}
data={idList!}
filename="bsedata.csv"
data-interception='off'
ref={csvInstance}
/>
</div>
)
}
export default CsvExample
Related
Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';
I've a jest test that is failing on addition of a new component to the page. The test is about showing of an error alert once error occurs. Code works in local environment but fails during commit.
Error Text:
TestingLibraryElementError: Unable to find an element with the text:
Student is unable to perform register/unregister activities.. This could be because
the text is broken up by multiple elements. In this case, you can
provide a function for your text matcher to make your matcher more
flexible.
Test:
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useMutation: jest.fn((_key, cb) => {
cb();
return { data: null };
})
}));
const useMutation = useMutationHook as ReturnType<typeof jest.fn>;
describe('StatusAlert', () => {
beforeEach(() => {
useMutation.mockReturnValue({});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should show error', () => {
useMutation.mockReturnValueOnce({
isError: true
});
const { getByText } = render(
<StudentRegister
students={[studentStub, studentStub]}
onSuccess={jest.fn()}
/>
);
expect(getByText(ErrorDict.ErrorRequest)).toBeInTheDocument();
});
StudentRegister:
Adding this component is causing the above mentioned error:
interface Props {
selectedStudents: Array<Student>;
onSuccessCallback: () => void;
}
export const StudentSelectionBar: FC<Props> = ({
selectedStudents,
onSuccessCallback
}) => {
const [isOpenDropCourseModal, setisOpenDropCourseModal] =
useState(false);
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) =>
student.id));
}, [selectedStudents]);
const onToggleOpenDropCourseModal = useCallback(() => {
setisOpenDropCourseModal(
(state) => !state
);
}, []);
const {
isError: isDropCourseError,
isSuccess: isDropCourseSuccess,
isLoading: isDropCourseLoading,
mutateAsync: DropCourseMutation,
error: DropCourseError
} = useMutation<void, ApiError>(
() => dropCourse(selectedStudents.map((student) =>
student.id)),
{
onSuccess() {
onToggleOpenDropCourseModal();
onSuccess();
}
}
);
return (
<>
<StatusAlert
isError={isDropCourseError}
isSuccess={isDropCourseSuccess}
errorMessage={
dropCourseError?.errorMessage ||
ErrorMessages.FailedPostRequest
}
successMessage="Students successfully dropped from
course"
/>
<StatusAlert
isError={registerMutation.isError}
isSuccess={registerMutation.isSuccess}
errorMessage={
registerMutation.error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully registered"
/>
<StatusAlert
isError={isError}
isSuccess={isSuccess}
errorMessage={
error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully unregistered"
/>
<Permissions scope={[DropCourseUsers]}>
<LoadingButton
color="error"
variant="contained"
onClick={onToggleDropCourseUserModal}
className={styles['action-button']}
loading={isDropCourseLoading}
loadingPosition="center"
disabled={registerMutation.isLoading || isLoading}
>
drop Course
</LoadingButton>
</Permissions>
<DropCourseModal
isOpen={isOpenDropCourseModal}
onCloseModal={onToggleOpenDropCourseModal}
onArchiveUsers={DropCourseMutation}
users={studentIds}
/>
</>
);
};
Update:
I've noticed that removing useEffect() hook from the component, makes it render correctly in the test. Its function is to update the state variable holding studentIds on every selection on the list.
Is there a way to mock following useEffect hook with dependency in the test?
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) => student.id));
}, [selectedStudents]);
I am pretty new to React, and the only CRUD functionality I have done has been in MVC.net, so I am pretty lost here and none of the tutorials I am finding seem to be the situation I have going on...or maybe I am just misunderstanding them. The tips I got with my initial question helped, but I am still no getting it to work, so hopefully now I have supplies enough info to help other help me. I did no include all of the input fields because their are like 15 and it was just redundant.
I am pulling up the modal using this onClick event:
onClick={()=> {handleEditModal(item)}}
modalFunctions.js
// Modal Functionality
export function ModalFunctions() {
const [selectedRecord, setSelectedRecord] = useState([]);
const [openModal, setOpenModal] = useState(false);
const handleEditModal = item =>{
setOpenModal(true)
setSelectedRecord(item)
}
return {
selectedRecord,
setSelectedRecord,
openModal,
setOpenModal,
handleEditModal,
handleDetailModal
}
}
// Form Functionality
export function FormFunctions(validateOnChange = false, validate) {
const [values, setValues] = useState('');
const [errors, setErrors] = useState({});
const handleInputChange = e => {
const { name, value } = e.target
setValues({
...values,
[name]: value
})
if (validateOnChange)
validate({ [name]: value })
}
return {errors, handleInputChange, setErrors, setValues, values}
}
DataTable.js
// Imported Modal Functions
const {
selectedRecord,
openModal,
setOpenModal,
handleEditModal,
handleDetailModal
} = ModalFunctions();
const baseURL = 'http://localhost:8080/api/tanks';
// Fetch Data
useEffect(() => {
const fetchData = async () =>{
setLoading(true);
try {
const {data: response} = await axios.get(baseURL);
setTableData(response);
} catch (error) {
console.error(error.message);
}
setLoading(false);
};
fetchData();
}, [baseURL, setTableData, setLoading]);
// The Modal
return(
<Modal
title={ "Editing: " + (selectedRecord.tankName) }
openModal={openModal}
setOpenModal={setOpenModal}
>
<TankEdit
selectedRecord={selectedRecord}
setOpenModal={setOpenModal}
openModal={openModal}
/>
</Modal>
)
TankEdit.js
export function TankEdit(props) {
const { baseURL, openModal, selectedRecord, setOpenModal, setTableData } = props;
const validate = (fieldValues = item) => {
let temp = { ...errors }
if ('tankName' in fieldValues)
temp.tankName = fieldValues.tankName ? "" : "This field is required."
setErrors({
...temp
})
if (fieldValues === values)
return Object.values(temp).every(x => x === " ")
}
const {
values,
setValues,
errors,
setErrors,
handleInputChange,
} = FormFunctions(true, validate);
useEffect(() => {
if (selectedRecord !== null)
setValues({
...selectedRecord
})
}, [selectedRecord, setValues])
function editRecord() {
axios.put(`${baseURL}`, {
title: "Success",
body: "The record had been successfully updated"
}).then ((response) => {setTableData(response.data);})
}
const handleSubmit = e => {
e.preventDefault()
if (validate()) {
editRecord(values);
}
setOpenModal(false)
}
const item = values; // used for easier referencing (matches table item)
return (
<Form onSubmit={handleSubmit} open={openModal}>
<Grid>
<Controls.Input
name="tankName"
label="Tank Name"
value={item.tankName}
onChange={handleInputChange}
error={errors.tankName}
/>
</Grid>
<Grid>
<Controls.Button
type="submit"
text="Submit"
/>
<Controls.Button
text="Cancel"
color="default"
onClick={()=>{setOpenModal(false)}}
/>
</Grid>
</Form>
)
}
Input.js
export default function Input(props) {
const { error=null, label, name, onChange, value, ...other } = props;
return (
<TextField
variant="outlined"
label={label}
name={name}
value={value}
defaultValue=''
onChange={onChange}
{...other}
{...(error && {error:true,helperText:error})}
/>
)
}
My company is only wanting a Read and an Update function, since Creating and Deletion will be handled another ways, so this is my final hangup. I think I am close, but I am missing something.
Can anyone point me in the right direction?
THANKS!!!!
If you want to write an update request you would use axios.put to send the data to your back-end.
In your handleSubmit function you do:
let response = await axios.put('http://-------/api/tanks', { name: 'tank' });
(The second parameter is an object that needs to contain all the form data fields)
Also make sure you call e.preventDefault() in the handleSubmit function so you don't accidentally navigate elsewhere.
Then you will update your database or whatever using your back-end.
for update you should use put or patch method
and you should send id of item you want to update in request url.
I insert 2 example here.
this is for put:
const res = await axios.put('/api/article/123', {
title: 'Making PUT Requests with Axios',
status: 'published'
});
this is for patch:
const res = await axios.patch('/api/article/123', {
title: 'Making PUT Requests with Axios',
status: 'published'
});
I get not wrapped in act error while testing my component. Has someone any idea how to solve it? I've already tried many things like wrapping findByTestAttr in waitFor but it didn't work. And btw my test doesn't fail, assertion is correct, i only get these warnings. I wonder if it's actually my fault or it's jest fault that this error shows at all?
// CurrencyConverter.tsx
const HAVE = 'have';
const RECEIVE = 'receive';
const CurrencyConverter: React.FC = () => {
const [haveInputValues, setHaveInputValues] = React.useState<CurrencyInputValues>({ currency: Currencies.PLN, value: 100 });
const [receiveInputValues, setReceiveInputValues] = React.useState<CurrencyInputValues>({ currency: Currencies.USD, value: '' });
const [inputsSwapped, setInputsSwapped] = React.useState<boolean>(false);
const [rate, setRate] = React.useState<number>(0.0);
const [status, setStatus] = React.useState<Status>({ wasChangedByUser: true, last: HAVE });
const iconsStyle = useStyles();
const getNewCurrencies = (currency: Currencies, type: string) => {
const isHaveType = type === HAVE;
const notChangedCurrency = isHaveType ? receiveInputValues.currency : haveInputValues.currency;
let newHaveCurrency: Currencies, newReceiveCurrency: Currencies;
if (currency === Currencies.PLN && notChangedCurrency === Currencies.PLN) {
newHaveCurrency = isHaveType ? Currencies.PLN : receiveInputValues.currency;
newReceiveCurrency = isHaveType ? haveInputValues.currency : Currencies.PLN;
} else {
newHaveCurrency = isHaveType ? currency : Currencies.PLN;
newReceiveCurrency = isHaveType ? Currencies.PLN : currency;
}
return { newHaveCurrency, newReceiveCurrency };
};
const changeCurrency = (currency: Currencies, type: string) => {
const { newHaveCurrency, newReceiveCurrency } = getNewCurrencies(currency, type);
setReceiveInputValues((currState) => ({ ...currState, currency: newReceiveCurrency }));
setHaveInputValues((currState) => ({ ...currState, currency: newHaveCurrency }));
setStatus({ last: type, wasChangedByUser: true });
};
const changeValue = (value: number | '', type: string) => {
const setter = type === HAVE ? setHaveInputValues : setReceiveInputValues;
setter((currState) => ({ ...currState, value }));
setStatus({ last: type, wasChangedByUser: true });
};
const swapInputs = () => {
setHaveInputValues(receiveInputValues);
setReceiveInputValues(haveInputValues);
setInputsSwapped((currState) => !currState);
setStatus({ last: status.last === HAVE ? HAVE : RECEIVE, wasChangedByUser: true });
};
React.useEffect(() => {
if (!status.wasChangedByUser) return;
const getPropsToCompare = () => {
const isHaveStatus = status.last === HAVE;
const value = isHaveStatus ? haveInputValues.value : receiveInputValues.value;
const fromCurrency = isHaveStatus ? haveInputValues.currency : receiveInputValues.currency;
const toCurrency = isHaveStatus ? receiveInputValues.currency : haveInputValues.currency;
return { value, fromCurrency, toCurrency };
};
const getNewComparisonData = async () => {
const { value, fromCurrency, toCurrency } = getPropsToCompare();
if (value) return await axios.get<CurrencyComparison>(`${Endpoints.COMAPRE_CURRENCIES}/${value}/${fromCurrency}/${toCurrency}/`);
const onEmptyInputData = {
data: {
result: {
exchangeAmount: 0,
exchangeRate: rate,
},
},
};
return onEmptyInputData;
};
const updateComparison = async () => {
const { data } = await getNewComparisonData();
setRate(+data.result.exchangeRate);
const setter = status.last === HAVE ? setReceiveInputValues : setHaveInputValues;
setter((currState: CurrencyInputValues) => ({ ...currState, value: +data.result.exchangeAmount }));
};
updateComparison();
setStatus((currState) => ({ ...currState, wasChangedByUser: false }));
}, [status.wasChangedByUser]);
return (
<div className={classes.currencyConverter}>
<div className={classes.inputsWithConnector}>
<CurrencyInput label={HAVE} values={{ ...haveInputValues, changeCurrency, changeValue }} />
<div className={classes.inputsConnector}>
<div className={classes.connectorLine}></div>
<SwapHorizIcon className={classnames(iconsStyle.swap, inputsSwapped && iconsStyle.swapRotated)} onClick={swapInputs} />
</div>
<CurrencyInput label={RECEIVE} values={{ ...receiveInputValues, changeCurrency, changeValue }} />
</div>
<p className={classes.rate}>
Current rate:{' '}
<span className={classes.rateValue} data-test='currency-rate'>
{rate}
</span>
</p>
</div>
);
};
export default CurrencyConverter;
// test
const setup = () => {
return mount(<CurrencyConverter />);
};
describe('<CurrencyConverter />', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
moxios.install(axiosInstance);
});
afterEach(() => {
moxios.uninstall(axiosInstance);
});
it('displays value in receive input and correct rate on page init', (done) => {
wrapper = setup();
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request
.respondWith({
status: 200,
response: {
result: {
exchangeRate: '4',
exchangeAmount: '25',
},
},
})
.then(async () => {
wrapper.update();
const receiveValueInput = findByTestAttr(wrapper, 'receive-value-input');
const rate = findByTestAttr(wrapper, 'currency-rate');
expect(rate.text()).toEqual('4');
expect(receiveValueInput.prop('value')).toEqual(25);
done();
});
});
});
});
// error
console.error
Warning: An update to CurrencyConverter inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at CurrencyConverter (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\src\components\home\infoCard\currencyConverter\CurrencyConverter.tsx:47:55)
at WrapperComponent (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\node_modules\#wojtekmaj\enzyme-adapter-utils\src\createMountWrapper.jsx:46:26)
127 | const { data } = await getNewComparisonData();
128 |
> 129 | setRate(+data.result.exchangeRate);
| ^
130 |
131 | const setter = status.last === HAVE ? setReceiveInputValues : setHaveInputValues;
132 | setter((currState: CurrencyInputValues) => ({ ...currState, value: +data.result.exchangeAmount }));
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-dom/cjs/react-dom.development.js:24064:9)
at setRate (node_modules/react-dom/cjs/react-dom.development.js:16135:9)
at _callee2$ (src/components/home/infoCard/currencyConverter/CurrencyConverter.tsx:129:7)
at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
console.error
Warning: An update to CurrencyConverter inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at CurrencyConverter (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\src\components\home\infoCard\currencyConverter\CurrencyConverter.tsx:47:55)
at WrapperComponent (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\node_modules\#wojtekmaj\enzyme-adapter-utils\src\createMountWrapper.jsx:46:26)
130 |
131 | const setter = status.last === HAVE ? setReceiveInputValues : setHaveInputValues;
Ok, it seems like I solved this problem by wrapping request.respondWith with waitFor. This entire act error is very annoying :/
it('displays value in receive input and correct rate on page init', (done) => {
wrapper = setup();
moxios.wait(async () => {
const request = moxios.requests.mostRecent();
await waitFor(() => {
request
.respondWith({
status: 200,
response: {
result: {
exchangeRate: '4',
exchangeAmount: '25',
},
},
})
.then(() => {
wrapper.update();
const receiveValueInput = findByTestAttr(wrapper, 'receive-value-input');
const rate = findByTestAttr(wrapper, 'currency-rate');
expect(rate.text()).toEqual('4');
expect(receiveValueInput.prop('value')).toEqual(25);
done();
});
});
});
});
I am having a data rendering issue in react. Somehow, data is not automatically updated after it's updated in the server side. I can't put all the code in here, cuz the code is kind of lengthy. so i pasted/renamed some variables. Even if some variables are missing, please understand. Basically, I have a button on the page and when the button is clicked, the status changes to 'UPLOADING' and the function checkIfDataExists is called to fetch data from the server side and data should be automatically updated without page refresh, but when I test this, data is successfully retrieved from the server side, but the updated data is not rendered. I see 'successful...' on the Console. Is there anything wrong?
const Settings: React.FC<IProps> = props => {
const { orgId } = props
const password = 'dummy'
const { data } = httpCall(`/${orgId}/${userId}/settings`)
return (
<div>
{data && <SettingsForm data={data} password={password} {...props} />}
</div>
)
}
const SettingsForm: React.FC<Settings & IProps> = ({
data,
password
}) => {
const [status, setStatus] = useState<'ERROR' | 'DONE' | 'UPLOADING'>()
const service = getServiceInstance(data.organizationId)
function checkIfDataExists(user: any) {
return () => {
httpCall
.getClient(user.id)
.then(value => {
console.log('successful...')
data.modeUsername = value.modeUsername
data.modePassword = value.modePassword
})
.catch(() => {
setStatus('ERROR')
})
}
}
useEffect(() => {
if (!status) return
switch (status) {
case 'UPLOADING': {
const timer = setInterval(
checkIfDataExists({ id: data.id }),
2000
)
return () => clearInterval(timer)
}
}
}, [status, client
])
<div className="info-section">
<p className="detail">Username</p>
<p>{data.modeUsername}</p>
</div>
<div className="info-section">
<p className="detail">Password</p>
<p>{data.modePassword}</p>
</div>
The problem I see is that after you setInterval an API you didn't set in the state to trigger the component to rerender. You don't need to be explicit to define resData to data because if you define data already useState already it types.
const SettingsForm: React.FC<Settings & IProps> = ({
data,
password
}) => {
const [resdata,setResData] = useState(data)
const [status, setStatus] = useState<'ERROR' | 'DONE' | 'UPLOADING'>()
const service = getServiceInstance(data.organizationId)
function checkIfDataExists(user: any) {
return () => {
httpCall
.getClient(user.id)
.then(value => {
console.log('successful...')
setResData({
modeUsername: value.modeUsername,
modePassword: value.modePassword,
})
// data.modeUsername = value.modeUsername
// data.modePassword = value.modePassword
})
.catch(() => {
setStatus('ERROR')
})
}
}
useEffect(() => {
if (!status) return
switch (status) {
case 'UPLOADING': {
const timer = setInterval(
checkIfDataExists({ id: data.id }),
2000
)
return () => clearInterval(timer)
}
}
}, [status, client
])
<div className="info-section">
<p className="detail">Username</p>
<p>{resdata.modeUsername}</p>
</div>
<div className="info-section">
<p className="detail">Password</p>
<p>{resdata.modePassword}</p>
</div>