How to reinitialize the state with react hook when rerender the components - javascript

I created a component to dispaly a question and its different options and when a user click the next button, a redirection to the same page will be executed in order to rerender the component and display a new question.
I use a checkBox component to display the question options but whenever I chose an option and go to the next question; the checkboxes are not reset for the new one (for example if I checked the second option for the first question, I get the second option checked in the second question before I checked it).
import Grid from "#material-ui/core/Grid";
import Typography from "#material-ui/core/Typography";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Dispatch } from "redux";
import { IAnswer, incrementQuestion, IQuestion, questionRequest } from "../../actions/index";
import CheckBoxWrapper from "../../components/common/CheckBoxWrapper";
import ContentQuiz from "../../components/ContentQuiz";
import history from "../../history/history";
interface IProps {
currentQuestionNumber: number;
loadingData: boolean;
questions: IQuestion[];
questionRequest: () => void;
incrementQuestion: (arg: IAnswer) => void;
numberOfQuestions: number;
}
interface IAnswerOption {
option1: boolean;
option2: boolean;
option3: boolean;
option4: boolean;
[key: string]: boolean;
}
const Quiz = (props: IProps) => {
const { currentQuestionNumber,
loadingData,
questions,
questionRequest,
incrementQuestion,
numberOfQuestions } = props;
const [answerOption, setAnswerOption] = useState<IAnswerOption>({
option1: false,
option2: false,
option3: false,
option4: false,
});
const handleChange = (option: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
setAnswerOption({ ...answerOption, [option]: event.target.checked });
};
useEffect(() => {
questionRequest();
});
const handleNextQuiz = () => {
if (currentQuestionNumber === numberOfQuestions - 1) {
history.push("/homepage");
} else {
incrementQuestion(answerOption);
history.push("/contentQuiz");
}
};
const currentQuestion = questions[currentQuestionNumber];
return (
<div>
{loadingData ? ("Loading ...") : (
< ContentQuiz
questionNumber={currentQuestionNumber + 1}
handleClick={handleNextQuiz} >
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark} >
{currentQuestion.description.replace(";", "\n")}
</SyntaxHighlighter >
<form>
<Grid container direction="column" alignItems="baseline">
{currentQuestion.options.map((option: string, index: number) => {
const fieldName = `option${index + 1}`;
return (
<Grid key={index}>
<CheckBoxWrapper
checked={answerOption[fieldName]}
value={fieldName}
onChange={handleChange(fieldName)}
label={option}
/>
</Grid>);
}
)}
</Grid>
</form>
</>
</div >
</ContentQuiz >
)}
</div>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, loadingData, questions, numberOfQuestions } = state.quiz;
return {
currentQuestionNumber,
loadingData,
questions,
numberOfQuestions
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
incrementQuestion: (answer: IAnswer) => dispatch<any>(incrementQuestion(answer)),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Quiz);
How can I reset the question options whenever I rerender the component, in order to check the options?

const fieldName = `option${index + 1}`;
Reset fieldName when you are going to the next question

I think you just need to fix your useEffect.
useEffect(() => {
setAnswerOption({
option1: false,
option2: false,
option3: false,
option4: false,
});
questionRequest();
}, []);
Also, do not forget to pass the second argument, otherwise you might have an infinite loop in your component. https://reactjs.org/docs/hooks-effect.html

Related

Conditional types without a shared param

I want to make my props be either type A, or B. For example
export default function App() {
type Checkbox = {
type: "checkbox";
checked: boolean;
};
type Dropdown = {
type: "dropdown";
options: Array<any>;
selectedOption: number;
};
type CheckboxOrDropdown = Checkbox | Dropdown;
const Component: FC<CheckboxOrDropdown> = (props) => {
return <>"...correct component"</>;
};
// these returns are just examples
return <Component type="checkbox" checked={true} />;
return <Component type="dropdown" options={[]} selectedOption={0} />;
}
Here's a fiddle
How can I achieve the same, but without the "type" prop? So that TS recognizes the type based on other props?
You can overload your component. By overloading here I mean intersection of two functional components:
import React, { FC } from 'react'
export default function App() {
type Checkbox = {
checked: boolean;
};
type Dropdown = {
options: Array<any>;
selectedOption: number;
};
const Component: FC<Checkbox> & FC<Dropdown> = (props) => {
return <>"...correct component"</>;
};
return [<Component checked={true} />, <Component options={[]} selectedOption={0} />];
}
This is the less verbose version I know.
If you have a lot of component types and you don't want to manually intersect them, you can use distributivity.
import React, { FC } from 'react'
export default function App() {
type Checkbox = {
checked: boolean;
};
type Dropdown = {
options: Array<any>;
selectedOption: number;
};
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Overload<T> = UnionToIntersection<T extends any ? FC<T> : never>
const Component: Overload<Checkbox | Dropdown> = (props) => {
return <>"...correct component"</>;
};
return [<Component checked={true} />, <Component options={[]} selectedOption={0} />];
}
Playground

Changing onClick function of React Component to React.FC

I have a Tab component which is a part of tabs structure, but I need to convert it into React.FC. Here is the original and below is what I've done so far, but I'm getting lost around the onclick functionality.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Tab extends Component {
static propTypes = {
activeTab: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};
onClick = () => {
const { label, onClick } = this.props;
onClick(label);
}
render() {
const {
onClick,
props: {
activeTab,
label
}
} = this;
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
return (
<li
className={className}
onClick={onClick}
>
{label}
</li>
);
}
}
export default Tab;
Here is my very bad attempt, which obviously is very bad
import React from 'react';
/**
* #function Tab
*/
const Tab: React.FC = () => {
type Props = {
activeTab: string;
label: string;
}
const onClick = (props) => {
const { label, onClick } = props;
onClick(label);
}
const {
onClick,
props: {
activeTab,
label
}
} = this;
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
return (
<li
className={className}
onClick={onClick}
>
{label}
</li>
);
}
export default Tab;
Any help would be much much appreciated, thank you!
If you are using typescript, you can define all the component props inside a type/interface and give it to the React.FC type, for example:
import React from 'react';
interface Props {
activeTab: string;
label: string;
onClick: (label: string) => void; // this means that the onClick param is a function that takes a label of type string as function parameter
}
// here we create a React functional component and we pass the Props interface to specify the component props
const Tab: React.FC<Props> = (props) => {
const handleOnClick = () => {
props.onClick(props.label)
}
let className = 'tab-list-item';
if (props.activeTab === props.label) {
className += 'tab-list-active';
}
return (
<li
className={className}
onClick={props.handleOnClick}
>
{props.label}
</li>
);
}
export default Tab;
If you know how to destructor an object you can clean your function in this way:
import React from 'react';
interface Props {
activeTab: string;
label: string;
onClick: (label: string) => void; // this means that the onClick param is a function that takes a label of type string as function parameter
}
// here we create a React functional component and we pass the Props interface to specify the component props
const Tab: React.FC<Props> = ({activeTab, label, onClick}) => {
const handleOnClick = () => {
onClick(label)
}
let className = 'tab-list-item';
if (props.activeTab === label) {
className += 'tab-list-active';
}
return (
<li
className={className}
onClick={handleOnClick}
>
{label}
</li>
);
}
export default Tab;

Redirect after posting to another page

I am working on a react coding template where after clicking on submit, it shows me a successful message and stays on the same page. I would like to add a redirect to the submit button if successful without changing the design. My ./AskPage component uses interfaces from ./Form. How do I add a redirect to the ./AskPage once I click on submit? Below is my code
./Form code is:
import { FC, useState, createContext, FormEvent } from 'react';
import { PrimaryButton, gray5, gray6 } from './Styles';
/** #jsx jsx */
import { css, jsx } from '#emotion/core';
export interface Values {
[key: string]: any;
}
export interface Errors {
[key: string]: string[];
}
export interface Touched {
[key: string]: boolean;
}
/* export interface onSubmit {
[key: string]: any
} */
interface FormContextProps {
values: Values;
setValue?: (fieldName: string, value: any) => void;
errors: Errors;
validate?: (fieldName: string) => void;
touched: Touched;
setTouched?: (fieldName: string) => void;
}
export const FormContext = createContext<FormContextProps>({
values: {},
errors: {},
touched: {},
});
type Validator = (value: any, args?: any) => string;
export const required: Validator = (value: any): string =>
value === undefined || value === null || value === ''
? 'This must be populated'
: '';
export const minLength: Validator = (value: any, length: number): string =>
value && value.length < length
? `This must be at least ${length} characters`
: '';
interface Validation {
validator: Validator;
arg?: any;
}
interface ValidationProp {
[key: string]: Validation | Validation[];
}
export interface SubmitResult {
success: boolean;
errors?: Errors;
}
interface Props {
submitCaption?: string;
validationRules?: ValidationProp;
onSubmit: (values: Values) => Promise<SubmitResult>;
successMessage?: string;
failureMessage?: string;
}
export const Form: FC<Props> = ({
submitCaption,
children,
validationRules,
onSubmit,
successMessage = 'Success!',
failureMessage = 'Something went wrong',
}) => {
const [values, setValues] = useState<Values>({});
const [errors, setErrors] = useState<Errors>({});
const [touched, setTouched] = useState<Touched>({});
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [submitError, setSubmitError] = useState(false);
const validate = (fieldName: string): string[] => {
if (!validationRules) {
return [];
}
if (!validationRules[fieldName]) {
return [];
}
const rules = Array.isArray(validationRules[fieldName])
? (validationRules[fieldName] as Validation[])
: ([validationRules[fieldName]] as Validation[]);
const fieldErrors: string[] = [];
rules.forEach(rule => {
const error = rule.validator(values[fieldName], rule.arg);
if (error) {
fieldErrors.push(error);
}
});
const newErrors = { ...errors, [fieldName]: fieldErrors };
setErrors(newErrors);
return fieldErrors;
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (validateForm()) {
setSubmitting(true);
setSubmitError(false);
const result = await onSubmit(values);
setErrors(result.errors || {});
setSubmitError(!result.success);
setSubmitting(false);
setSubmitted(true);
}
};
const validateForm = () => {
const newErrors: Errors = {};
let haveError: boolean = false;
if (validationRules) {
Object.keys(validationRules).forEach(fieldName => {
newErrors[fieldName] = validate(fieldName);
if (newErrors[fieldName].length > 0) {
haveError = true;
}
});
}
setErrors(newErrors);
return !haveError;
};
return (
<FormContext.Provider
value={{
values,
setValue: (fieldName: string, value: any) => {
setValues({ ...values, [fieldName]: value });
},
errors,
validate,
touched,
setTouched: (fieldName: string) => {
setTouched({ ...touched, [fieldName]: true });
},
}}
>
<form noValidate={true} onSubmit={handleSubmit}>
<fieldset
disabled={submitting || (submitted && !submitError)}
id="fieldset" >
{children}
<div id="children" >
<PrimaryButton type="submit">{submitCaption}</PrimaryButton>
</div>
{submitted && submitError && (
<p id="failure" >
{failureMessage}
</p>)}
{submitted && !submitError && (
<p id="success" >
{successMessage}
</p>)}
</fieldset>
</form>
</FormContext.Provider>
);
};
My ./AskpPage is:
import React, { useState, FC } from 'react';
import { Page } from './Page';
import { Form, required, minLength, Values } from './Form';
import { Field } from './Field';
import { postQuestion } from './QuestionsData';
import { BrowserRouter, Route, Redirect, Switch, Link } from 'react-router-dom';
import { isPropertySignature } from 'typescript';
export const AskPage = () => {
const handleSubmit = async (values: Values) => {
const question = await postQuestion({
title: values.title,
content: values.content,
userName: 'Fred',
created: new Date(),
});
return { success: question ? true : false };
};
return (
<Page title="Ask a Question">
{/* I want to add a redirect to this form once I click Submit Your Question */}
<Form
submitCaption="Submit Your Question"
validationRules={{
title: [{ validator: required }, { validator: minLength, arg: 10 }],
content: [{ validator: required }, { validator: minLength, arg: 20 }],
}}
onSubmit={handleSubmit}
failureMessage="There was a problem with your question"
successMessage="Your question was successfully submitted"
>
<Field name="title" label="Title" />
<Field name="content" label="Content" type="TextArea" />
</Form>
</Page>
);
};
export default AskPage;
I use this code example to redirect after my form submissions in react. I basically do all that is needed and save the answer then call the appropriate function to take me to an appropriate page. That page may then go get the answer. Another way would be to use props and import another page...
function viewPage() {
let url = window.location.origin;
console.log('viewPage', url)
url=url.concat('/SelfAssessment')
window.open(url, "_blank");
}

useRef is undefined

I want to call a child functional component's method from a parent functional component. I've created useRef and put it via props to the child component. I've wrapped the child component with forwardRef. But I get an undefined ref.
Also, I have no way to put the ref to the dom element (only to functional component).
The parent component:
import Tree from '#c-tree'
import React, {
FunctionComponent,
useEffect,
useState,
useContext,
useRef,
} from 'react';
const NavTree: FunctionComponent = () => {
const refTree = useRef();
useEffect(() => {
if (refTree !== undefined && refTree.current !== undefined) {
// #ts-ignore
console.log(refTree.current.handleCurrentSelected);
}
// eslint-disable-next-line
}, [refTree]);
const tree = () => {
if (d?.items) {
return (
<Tree ref={refTree}>
{d.items.map(s => (
<Tree.Asset
key={s.id}
name={s.name}
dataSelected={`${s.id}`}
item={{
name: s.name,
tenantID: s.id,
type: s?.tree?.name,
children: [],
}}
/>
))}
</Tree>
);
}
};
return (
<SideBar
title={title}
>
<Box display="flex" flexDirection="column" height="100%">
<StyledBox>{tree()}</StyledBox>
</Box>
**</SideBar>
);
};
export default NavTree;
The child component:
import React, { useImperativeHandle, useState, ForwardRefExoticComponent, forwardRef, PropsWithRef } from 'react';
import TreeContext from './treeContext';
import TreeGroup from './components/TreeGroup';
import TreeEntity from './components/TreeEntity';
interface TabsStatic {
Group: typeof TreeGroup;
Asset: typeof TreeEntity;
}
type TabsComponent = ForwardRefExoticComponent<PropsWithRef<ITreeProps>> & TabsStatic;
interface ITreeProps {
data?: {
type: string;
name: string;
tenantID: number;
children?: Array<Object>;
}[];
ref?: any;
}
export const Tree = forwardRef(({
data,
}: ITreeProps, ref) => {
const [contextValues, setContextValues] = useState({
selected: null,
opened: {},
});
useImperativeHandle(ref, () => (
{
handleCurrentSelected: (selectedName: string) => {
setContextValues({
...contextValues,
selected: selectedName,
opened: {
...contextValues.opened,
[selectedName]: !contextValues.opened[selectedName] || false,
},
});
}}
));
return (
<TreeContext.Provider value={contextValues}>
{
React.Children.map(children, child => {
return React.cloneElement(child, childrenProps);
})
}
</TreeContext.Provider>
);
}) as TabsComponent;
Tree.Group = TreeGroup;
Tree.Asset = TreeEntity;
export default Tree;
UPDATED
I can get useRef.current value after re-rendering. How can I get a useRef current (not previous) value?

In what condition it will re-render while using react custom hooks

I tried a sample in using react hook, make it a custom hook.
The problem is the simple hook useCount() goes fine, but the hook useCarHighlight() intending to switch highlight line would not cause re-render.
I see it is the same of the two, is anything wrong I should attention for about this?
I made a sandbox here: https://codesandbox.io/s/typescript-j2xtf
Some code below:
// index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import useCarHighlight, { Car } from "./useCarHighlight";
import useCount from "./useCount";
const myCars: Car[] = [
{ model: "C300", brand: "benz", price: 29000, ac: "auto ac" },
{ model: "Qin", brand: "byd", price: 9000 }
];
const App = () => {
const { cars, setHighlight } = useCarHighlight(myCars, "Qin");
const { count, increase, decrease } = useCount(10);
console.log(
`re-render at ${new Date().toLocaleTimeString()},
Current highlight: ${
cars.find(c => c.highlight)?.model
}`
);
return (
<div>
<ul>
{cars.map(car => {
const { model, highlight, brand, price, ac = "no ac" } = car;
return (
<li
key={model}
style={{ color: highlight ? "red" : "grey" }}
>{`[${brand}] ${model}: $ ${price}, ${ac}`}</li>
);
})}
</ul>
<button onClick={() => setHighlight("C300")}>highlight C300</button>
<button onClick={() => setHighlight("Qin")}>highlight Qin</button>
<hr />
<h1>{`Count: ${count}`}</h1>
<button onClick={() => increase()}>+</button>
<button onClick={() => decrease()}>-</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
// useCarHighlight.ts
import { useState } from "react";
export type Car = {
model: string;
brand: string;
price: number;
ac?: "auto ac" | "manual ac";
};
export default function(
initialCars: Car[],
initialSelectedModel: string
): {
cars: Array<Car & { highlight: boolean }>;
setHighlight: (selMod: string) => void;
} {
const carsHighlight = initialCars.map(car => ({
...car,
highlight: initialSelectedModel === car.model
}));
const [cars, setCars] = useState(carsHighlight);
const setHighlight = (selMod: string) => {
cars.forEach(car => {
car.highlight = car.model === selMod;
});
setCars(cars);
};
return {
cars,
setHighlight
};
}
// useCount.ts
import { useState } from "react";
export default function useCount(initialCount: number) {
const [state, setState] = useState(initialCount);
const increase = () => setState(state + 1);
const decrease = () => setState(state - 1);
return {
count: state,
increase,
decrease
};
}
Unlike class components, mutating state of hooks does not queue a re-render, when using hooks you have to update your state in an immutable way.
Also, when calculating the next state based on the previous state it is recommended to use a functional update and read the previous state from the first argument of the function.
const setHighlight = (selMod: string) => {
setCars(prevState =>
prevState.map(car => ({
...car,
highlight: car.model === selMod
}))
);
};
Here is a good resource about immutable update patterns
Dont use forEach in setHighlight, use map instead
const setHighlight = (selMod: string) => {
const newCars = cars.map(car => ({
...car,
highlight: car.model === selMod
}));
setCars(newCars);
};
Use map instead of forEach as the address of car object isn't getting changed when you update highlight property in car.
const setHighlight = (selMod: string) => {
let carsTemp = cars.map(car => ({
...car,
highlight : car.model === selMod
}));
setCars(carsTemp);};

Categories

Resources