I'm building a site useing graphql and apollo with a react front end. I have created a page where the site admin can update the content for particular sections of pages and it works, but I keep getting the error in the console: Component is changing a controlled input to be uncontrolled...
I'm also using ReactQuill wysiwyg editor. I thought that might be the problem but I remved it and I'm still getting the same error.
Here is the code for the content update page:
import { useState, useEffect } from 'react';
import { useMutation, useQuery } from '#apollo/client';
import { useNavigate, useParams } from 'react-router-dom';
import { GET_CONTENT_BY_ID } from '../../utils/queries';
import { UPDATE_CONTENT } from '../../utils/mutations';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
const Content = () => {
const { id } = useParams();
const { loading, data } = useQuery(GET_CONTENT_BY_ID, {
variables: { id: id },
});
const [contentHead, setContentHead] = useState('');
const [contentText, setContentText] = useState('');
useEffect(() => {
const content = data?.contentById || {};
if (content) {
setContentHead(content.contentHead);
setContentText(content.contentText);
}
}, [data, setContentHead, setContentText]);
const [updateContent, { error }] = useMutation(UPDATE_CONTENT);
const navigate = useNavigate();
const submitHandler = async (e) => {
e.preventDefault();
try {
const { data } = await updateContent({
variables: {
id,
contentHead,
contentText,
},
});
navigate('/_admin');
} catch (error) {
console.log(error);
}
};
return (
<Form onSubmit={submitHandler}>
<Form.Group className="mb-3" controlId="contentHead">
<Form.Label>Section Heading</Form.Label>
<Form.Control
value={contentHead}
onChange={(e) => setContentHead(e.target.value)}
required
/>
</Form.Group>
<Form.Group className="mb-3" controlId="contentText">
<Form.Label>Text</Form.Label>
<ReactQuill
name="contentText"
value={contentText}
theme="snow"
onChange={setContentText}
/>
</Form.Group>
<Button type="submit">Submit</Button>
</Form>
);
};
export default Content;
In the ReqctQuill I tried onChange={(e) => contentText(e.target.value)} but there wasn't any change. The way it is now is what I got from, I think, their git repository.
I found an answer in another question on here. It wasn't the accepted answer but it works to get ride of the error.
A component is changing an uncontrolled input of type text to be controlled error in ReactJS
So for my form I change value={contentHead} and value={contentText} to value={contentHead || ''} and value={contentText || ''} and that works and gets rid of the error!
Related
I'm trying to make the place order in the ecommerce website I'm trying to make for my personal project. I wanted after I created the data or input the data I have made I get that Id and Redirect it to the orders/[id] then the Id.
Here is my code:
import React, { useContext, useState } from "react";
import { FETCH_USER_QUERY } from "../util/graphql/Queries";
import { useMutation, useQuery } from "#apollo/react-hooks";
import { AuthContext } from "../context/auth";
import { CartContext } from "../context/cart/CartContext";
import { Form, Button } from "semantic-ui-react";
import { CREATE_ORDER_MUTATION } from "../util/graphql/Mutations";
import { useRouter } from "next/router";
export default function Checkout() {
const router = useRouter();
const [cartItems, setCartItems] = useContext(CartContext);
const [paymentMethod, setPaymentMethod] = useState("");
const [address, setAddress] = useState("");
const [createOrder, { data, loading }] = useMutation(CREATE_ORDER_MUTATION);
const qty = cartItems.map(({ quantity }) => {
return quantity;
});
const cartItemId = cartItems.map(({ id }) => {
return id;
});
function onSubmit() {
createOrder({
variables: {
qty: qty[0],
products: cartItemId[0],
paymentMethod: paymentMethod,
address: address,
},
})
.then(() => {
setTimeout(() => {
const { createOrder: order } = { ...data };
console.log(order?.id);
}, 500);
})
}
return (
<>
<Form onSubmit={onSubmit} className={loading ? "loading" : ""}>
<h2>Create a Main Category:</h2>
<Form.Field>
<Form.Input
placeholder="Please Enter Address"
name="address"
label="Address: "
onChange={(event) => {
setAddress(event.target.value);
}}
value={address}
/>
<label>Status: </label>
<select
name="category"
className="form-control"
onChange={(event) => {
setPaymentMethod(event.target.value);
}}
>
<option value="Cash On Delivery">Cash On Delivery</option>
<option value="PayPal">PayPal</option>
<option value="GCash">GCash</option>
</select>
<br />
<Button type="submit" color="teal">
Submit
</Button>
</Form.Field>
</Form>
</>
);
}
But after I submitted my inputted data the log returns me undefined, but when I input data again and submit it gives me the previous id of that data. Is there any way to do this? If you don't understand what I mean please let me know I'll explain in the comments, or if you need any code I could give it to you I will be transparent as I can
Instead of using SetTimeOut(), you can simply use the onCompleted() function in Apollo graphql to perform anything whenever the mutation is successfully completed.
const [createOrder, { data, loading }] = useMutation(CREATE_ORDER_MUTATION, {
variables: {
},
onCompleted(data) {
console.log(data) // this is log all queries from the mutation including the ID you need
// whatever you want to do when the mutation is successful
router.push({
pathname: '/any-page',
query: {CartID: data.cart_id}, // check the console.log to see how the ID is returned });
}});
You forgot to return the res parameter from your promise. It should look something like this:
.then((res) => {
setTimeout(() => {
const { createOrder: order } = { ...res };
console.log(order?.id);
}, 500);
})
The issue is being caused by stale closure. The object reference of data at the time of setTimeout being pushed into the callback queue is an older one. Hence the value was never refreshed. You need to get the newer value by dispatch the action on the same previous state or using a useRef.
The onClickHandler in the following code, in this component, 'SearchResult', sometimes work and sometimes not.
I can't figure out any logic that can explain why it works when it works, and why it's not working, when it's not working.
I've put a debugger inside the onClickHandler, at the beginning of it, and when it's not working, it doesn't get to the debugger at all - what indicates that the function sometimes isn't even called, and I can't figure out why.
Furthermore, I've tried to move all the code in function to the onClick, inline, but then, it's not working at all.
In addition, I've tried to use a function declaration instead of an arrow function, and it still behaves the same - sometimes it works, and sometimes it's not...
This is the site, you can see the behavior for yourself, in the search box.
This is the GitHub repository
Here you can see a video demonstrating how it's not working, except for one time it did work
Please help.
The problematic component:
import { useDispatch } from 'react-redux'
import { Col } from 'react-bootstrap'
import { getWeatherRequest } from '../redux/weather/weatherActions'
import { GENERAL_RESET } from '../redux/general/generalConstants'
const SearchResult = ({ Key, LocalizedName, setText }) => {
const dispatch = useDispatch()
const onClickHandler = () => {
dispatch({ type: GENERAL_RESET })
dispatch(
getWeatherRequest({
location: Key,
cityName: LocalizedName,
})
)
setText('')
}
return (
<Col className='suggestion' onClick={onClickHandler}>
{LocalizedName}
</Col>
)
}
export default SearchResult
This is the parent component:
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Form } from 'react-bootstrap'
import { getAutoCompleteResultsRequest } from '../redux/autoComplete/autoCompleteActions'
import { AUTO_COMPLETE_RESET } from '../redux/autoComplete/autoCompleteConstants'
import SearchResult from './SearchResult'
const SearchBox = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const autoComplete = useSelector((state) => state.autoComplete)
const { results } = autoComplete
const onChangeHandler = (e) => {
if (e.target.value === '') {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}
setText(e.target.value)
dispatch(getAutoCompleteResultsRequest(e.target.value))
}
const onBlurHandler = () => {
setTimeout(() => {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}, 100)
}
return (
<div className='search-box'>
<Form inline>
<div className='input-group search-md search-sm'>
<input
type='search'
name='q'
value={text}
onChange={onChangeHandler}
onBlur={onBlurHandler}
placeholder='Search Location...'
className='mr-sm-2 ml-sm-3 form-control'
/>
</div>
</Form>
<div className='search-results'>
{results &&
results.map((result) => {
return (
<SearchResult key={result.Key} {...result} setText={setText} />
)
})}
</div>
</div>
)
}
export default SearchBox
I played a bit with your code and it looks like a possible solution may be the following addition in the SearchResult.js:
const onClickHandler = (e) => {
e.preventDefault();
...
After some tests
Please remove the onBlurHandler. It seams to fire ahaed of the onClickHandler of the result.
Can you put console.log(e.target.value) inside the onChangeHandler,
press again search results and make sure that one of it doesn't working and show us the console.
In searchResult component print to the console LocalizedName as well
I have been doing integration tests of my project's front-end pages with jest and react-testing-library, and have come accross an error I don't fully understand.
The page I am testing renders a formik form that the users inputs data for an commercial institution:
import React from 'react';
import {
FormGroup, Label, Row,
} from 'reactstrap';
import {
Field, getIn, FieldArray, useFormikContext,
} from 'formik';
import { useRecoilValue } from 'recoil';
import Select from 'react-select';
import {
institutionTypeAtom,
} from 'recoil/institution';
import { Colxx } from 'components/common/CustomBootstrap';
import IntlMessages from 'helpers/IntlMessages';
import { FormikStep } from 'components/Stepper/Step';
const InstitutionStep1 = ({ label, isView, validationSchema }) => {
const {
values, errors, touched, setFieldValue,
} = useFormikContext();
const types = useRecoilValue(institutionTypeAtom);
const typeFind = (values) => {
types.find((t) => t.institution_type_id === values.institution_type_id);
return values.institution_type_id;
};
return (
<FormikStep>
<Colxx xxs="4">
<FormGroup>
<Label>
<IntlMessages id="institution.type" />
</Label>
<Select
placeholder="Selecione o tipo"
value={types.find((t) => t.institution_type_id === values.institution_type_id)}
isDisabled={isView}
onChange={(selectedType) => {
setFieldValue('institution_type_id', selectedType.institution_type_id);
}}
options={types}
className="react-select"
classNamePrefix="react-select"
name="institution_type_id"
aria-label="institution_type_id"
getOptionLabel={(option) => option.institution_type}
getOptionValue={(option) => option.institution_type_id}
/>
{getIn(errors, 'institution_type_id') && getIn(touched, 'institution_type_id') && (
<div className="invalid-feedback d-block">
{getIn(errors, 'institution_type_id')}
</div>
)}
</FormGroup>
</Colxx>
</Row>
</FormikStep>
);
};
export default InstitutionStep1;
and one of the input fields is a select where he can select a type for the institution (private, public, foreign...). I am also using a stepper, and formikStep is the final component from that stepper that receives the initialValues:
export const institutionInitialValues: InitialValues<BaseInstitutionType> = (institution = {}) => ({
institution_type_id: institution.institution_type_id ? institution.institution_type_id : '',
deleted: !!institution.deleted,
});
And I have written my test to get the users select from the rendered page, but I am also very new to the testing area so I am referencing other tests that were on this project already:
import React from 'react';
import userEvent from '#testing-library/user-event';
import {
cleanup, screen, act, render, waitFor, renderWithRecoilSnapshot,
} from 'testWrapper';
import * as institution from 'services/adminModules/institution';
import {
generateMockInstitution, mockAxiosResponse,
} from 'helpers/testMocks';
import { institutionTypeAtom } from 'recoil/institution';
import InstitutionForm from '../../views/app/admin/InstitutionsPage/InstitutionForm';
let createInstitution: jest.SpyInstance;
const mockInstitution = generateMockInstitution();
const mockSubmit = jest.fn();
const mockClose = jest.fn();
const renderComponent = () => renderWithRecoilSnapshot(<InstitutionForm
handleClose={mockClose}
handleSubmit={mockSubmit}
/>,
({ set }) => {
set(institutionTypeAtom, 1);
});
describe('InstitutionFormPage', () => {
afterEach(cleanup);
beforeEach(() => {
createInstitution = jest.spyOn(institution, 'createInstitution').mockImplementation(async () => mockAxiosResponse());
jest.spyOn(institution, 'getInstitution').mockImplementation(async () => mockAxiosResponse());
});
it('should submit institution data', async () => {
renderComponent();
await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument());
screen.logTestingPlaygroundURL();
const institution_type_id = screen.getByRole('select', { name: 'institution_type_id' });
expect(institution_type_id).toBeInTheDocument();
await act(async () => {
await userEvent.selectOptions(institution_type_id, mockInstitution.institution_type_id);
});
const button = screen.getByRole('button', { name: /salvar/i });
expect(button).toBeInTheDocument();
await act(async () => {
await userEvent.click(button);
});
await waitFor(() => expect(mockSubmit).toHaveBeenCalledWith(mockInstitution));
});
});
Seemingly, the test is correct, but I am getting an error from when I run it's test that says that a .find I am using, in the component, is not a function.
So my main question would be, does this mean the test is correct and is appropriately catching a code error? Or is it wrong and it's not able to proceed? Also, the code it complains about works fine, and I was taught that an integration test tests api requests, so is there a reason why the test would complain about it, but the code would work fine?
So I'm forking off a sample Twilio video chat app (https://github.com/twilio/twilio-video-app-react). For the chat feature, Snackbar messages are employed. Which works fine. But I want to allow the user to send a message starting with http, so that the message will then be sent as a hyperlink URL.
The ChatInput component works fine for displaying this message as a hyperlink URL for the local user (i.e. - the sender). But the DataTrack event handler for the remote users doesn't display the message as a hyperlink. Just displays the literal text.
Here is the ChatInput.tsx, where any message starting with http will show up correctly for the local user.
import React, { useState } from 'react';
import { Button, FormControl, TextField } from '#material-ui/core';
import { useSnackbar } from 'notistack';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
export default function ChatInput() {
const [message, setMessage] = useState('');
const { room } = useVideoContext();
const { enqueueSnackbar } = useSnackbar();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setMessage(e.target.value);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (message) {
// Get the LocalDataTrack that we published to the room.
const [localDataTrackPublication] = [...room.localParticipant.dataTracks.values()];
// Construct a message to send
const fullMessage = `${room.localParticipant.identity} says: ${message}`;
if (message.startsWith('http')) {
// Send the message
localDataTrackPublication.track.send(`${message}`);
// Render the message locally so the local participant can see that their message was sent.
enqueueSnackbar(<a href={message}>{message}</a>);
} else {
// Send the full message
localDataTrackPublication.track.send(fullMessage);
// Render the full message locally so the local participant can see that their message was sent.
enqueueSnackbar(fullMessage);
}
//Reset the text field
setMessage('');
}
};
return (
<form autoComplete="off" style={{ display: 'flex', alignItems: 'center' }} onSubmit={handleSubmit}>
<FormControl>
<label htmlFor="chat-snack-input" style={{ color: 'black' }}>
Say something:
</label>
<TextField value={message} autoFocus={true} onChange={handleChange} id="chat-snack-input" size="small" />
</FormControl>
<Button type="submit" color="primary" variant="contained" style={{ marginLeft: '0.8em' }}>
Send
</Button>
</form>
);
}
And here is the DataTrack.ts, which only displays the string literal for any remote user.
import { useEffect } from 'react';
import { DataTrack as IDataTrack } from 'twilio-video';
import { useSnackbar } from 'notistack';
var stringToHTML = function (str) {
var parser = new DOMParser();
var doc = parser.parseFromString(str, 'text/html');
return doc.body;
};
export default function DataTrack({ track }: { track: IDataTrack }) {
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
const handleMessage = (message: string) => {
if (message.startsWith('http')) {
const newMessage = stringToHTML(message);
enqueueSnackbar(newMessage);
}
else {
enqueueSnackbar(message); }
};
track.on('message', handleMessage);
return () => {
track.off('message', handleMessage);
};
}, [track, enqueueSnackbar]);
return null; // This component does not return any HTML, so we will return 'null' instead.
}
Any suggestions as to how I can get the remote users to receive the same hyperlink URL that the sender sees?
TL;DR: the function stringToHTML is returnign a DOM element reference not a React element when passing the message to NotiStack, try wrapping it with a React element:
//const newMessage = stringToHTML(message);
enqueueSnackbar(<div dangerouslySetInnerHTML={{__html:message}} />);
NL;PR: And/Or I'm not sure why your are passing the message value to NotiStack differently in the two components:
if (message.startsWith('http')) {
//local user
enqueueSnackbar(<a href={message}>{message}</a>); //1)
//vs. remote user
const newMessage = stringToHTML(message);
enqueueSnackbar(newMessage); // should it be the same as 1)?
Appreciate the feedback. I was handling the message two different ways depending if a URL was being passed along or not. Actually the project author actually provided a clean solution. Installing the Linkify package allows just those string elements inferred to be HTML to be formatted as such.
Here is the reworked DataTrack.tsx contents. Works like a champ!
import React from 'react';
import { useEffect } from 'react';
import { DataTrack as IDataTrack } from 'twilio-video';
import { useSnackbar } from 'notistack';
import Linkify from 'react-linkify';
export default function DataTrack({ track }: { track: IDataTrack }) {
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
const handleMessage = (message: string) =>
enqueueSnackbar(
<Linkify
componentDecorator={(decoratedHref, decoratedText, key) => (
<a target="_blank" rel="noopener" href={decoratedHref} key={key}>
{decoratedText}
</a>
)}
>
{message}
</Linkify>
);
track.on('message', handleMessage);
return () => {
track.off('message', handleMessage);
};
}, [track, enqueueSnackbar]);
return null; // This component does not return any HTML, so we will return 'null' instead.
}
Currently without TypeScript this code is working, but now it is not working unfortunately. It gave me the following error: Property 'value' does not exist on type 'HTMLElement'. Not sure what is wrong with this. Seems it is nagging about the value. In this case I am using Jest testing and React. Not sure if I can ignore this error or should fix this in order to avoid weird bugs in the future.
import React from 'react';
import axios from 'axios';
import { useDispatch } from "react-redux";
import { getData } from '../../../actions/index';;
export const SearchInput : React.FC = () => {
const dispatch = useDispatch();
let input: any;
const getInputValue = (value: string):void => {
let url = `https://api.tvmaze.com/search/shows?q=${value}`
}
return (
<div className="container">
<h1>Keyword</h1>
<form className="form display-inline-flex"
onSubmit={e => {
e.preventDefault()
if(!input.value.trim()) return;
getInputValue(input.value);
}}>
<input className="form-input-field disable-outline display-inline"
ref={node => (input = node)}
placeholder="Search catalog"
aria-label="search-input"
/>
<button type="submit" className="btn btn-grey white-color display-inline">
Search
</button>
</form>
</div>
)
}
export default SearchInput;
// Jest testing
import React from "react"
import { render, fireEvent } from "#testing-library/react";
import { SearchInput } from "./SearchInput";
import { Provider } from "react-redux";
import { store } from "../../../Store";
const setup = () => {
const utils = render(
<Provider store={store}>
<SearchInput/>
</Provider>);
const input = utils.getByLabelText("search-input");
return {
input,
...utils
}
}
test("It should check if input field get the value passed", () => {
const { input } = setup();
fireEvent.change(input, { target: { value: "search-bar-test" } })
expect(input.value).toBe("search-bar-test")
});
You should be good to go if you add a type assertion like:
const input = utils.getByLabelText("search-input") as HTMLInputElement;