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?
Related
I'm using ant design pro.
The idea is In the platform we have 2 languages to choose from Fr(French) and En(English),
I want the user when he logs in and change the language to English for example when he logs out and log in again the language should be saved to English so he would be able to see the content in English, I managed to do it, when i login in the backend the preferedLanguage = en, the language toggle in the also changes to en, the only problem the web page content stays in French it's only change in English when i reload the page.
I think the issue is related to the login page, the login page is set to French as default , let's say my preferred language now is English, if i login from the login page. The page content loaded in French only changes when i reload.
-This is the umijs documentation : https://umijs.org/docs/max/i18n#setlocale-%E8%AE%BE%E7%BD%AE%E8%AF%AD%E8%A8%80
-LanguageDropdown (the toggle where you select the language (Fr or En)
import type { FC } from 'react';
import React, { useState, useEffect } from 'react';
import { Menu, Dropdown } from 'antd';
import { CaretDownOutlined, CaretUpOutlined, GlobalOutlined } from '#ant-design/icons';
import styles from './index.less';
import { setLocale, getLocale, getAllLocales } from 'umi';
interface Props {
setUpdateLang: any;
currentLang: string;
}
const LanguageDropdown: FC<Props> = ({ currentLang, setUpdateLang }) => {
const [langvisible, setLangVisible] = useState<boolean>(false);
const [localesList, setLocalesList] = useState<any[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>('');
// useEffect(() => {
// if (currentLang) {
// setLocalesList(getAllLocales());
// setCurrentLocale(getLocale());
// setLocale(currentLang === 'fr' ? 'fr-FR' : 'en-US');
// }
// alert(currentLang);
// alert(getLocale());
// }, [currentLang]);
useEffect(() => {
setLocalesList(getAllLocales());
setCurrentLocale(getLocale());
if (currentLang) {
// alert(222);
const selectedLang = currentLang === 'fr' ? 'fr-FR' : 'en-US';
// setNewLang(selectedLang);
setLocale(selectedLang, false);
setCurrentLocale(getLocale());
}
}, [currentLang]);
const onLangVisibleChange = (visibleLang: boolean) => {
setLangVisible(visibleLang);
};
const langNameHandler = (lang: string) => {
if (lang === 'en-US') return 'EN';
else if (lang === 'fr-FR') return 'FR';
return 'FR';
};
const setNewLang = (lang: string) => {
setUpdateLang({ lang: langNameHandler(lang).toLocaleLowerCase(), updated: true });
setLocale(lang);
};
const Langmenu = (
<Menu>
{localesList?.map((lang: any) => (
<Menu.Item key={lang}>
<a onClick={() => setNewLang(lang)}>{langNameHandler(lang)}</a>
</Menu.Item>
))}
</Menu>
);
return (
<div className={styles.profileDropdownContainer}>
<Dropdown
overlay={Langmenu}
placement="bottomLeft"
trigger={['click']}
onVisibleChange={onLangVisibleChange}
className={styles.dropdown}
>
<div className={styles.langContainer}>
<span>
<GlobalOutlined /> {langNameHandler(currentLocale)}
</span>
{!langvisible ? <CaretDownOutlined /> : <CaretUpOutlined />}
</div>
</Dropdown>
</div>
);
};
export default LanguageDropdown;
-RightContext
import { Tag } from 'antd';
import type { Settings as ProSettings } from '#ant-design/pro-layout';
import React, { useEffect, useState } from 'react';
import type { ConnectProps } from 'umi';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { ConnectState } from '#/models/connect';
import Avatar from './AvatarDropdown';
import styles from './index.less';
import LanguageDropdown from '../languageDropdown';
import moment from 'moment';
export type GlobalHeaderRightProps = {
dispatch: Dispatch;
theme?: ProSettings['navTheme'] | 'realDark';
auth: any;
users: any;
platformLanguage: any;
data: any;
} & Partial<ConnectProps> &
Partial<ProSettings>;
const ENVTagColor = {
dev: 'orange',
test: 'green',
pre: '#87d068',
};
const GlobalHeaderRight: React.FC<GlobalHeaderRightProps> = (props) => {
const [updateLang, setUpdateLang] = useState<{ lang: string; updated: boolean }>({
lang: '',
updated: false,
});
const [currentLang, setCurrentLang] = useState<any>(null);
const { theme, layout, auth, platformLanguage, data, dispatch } = props;
let className = styles.right;
useEffect(() => setCurrentLang(platformLanguage), [platformLanguage]);
useEffect(() => {
if (updateLang.updated) {
const {
organization,
roles,
email,
deleted,
department,
createdById,
organizationId,
...rest
} = data;
const birthdate = moment(rest.birthdate).format('YYYY-MM-DD');
const workversary = moment(rest.workversary).format('YYYY-MM-DD');
dispatch({
type: 'users/updateMe',
payload: {
data: { ...rest, birthdate, workversary, platformLanguage: updateLang.lang },
userId: auth?.currentUser.id,
},
});
setUpdateLang({ ...updateLang, updated: false });
setCurrentLang(updateLang.lang);
}
}, [updateLang]);
if (theme === 'dark' && layout === 'top') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
{currentLang ? (
<LanguageDropdown currentLang={currentLang} setUpdateLang={setUpdateLang} />
) : null}
<Avatar />
{REACT_APP_ENV && (
<span>
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
</span>
)}
</div>
);
};
export default connect(({ settings, auth, users }: ConnectState) => ({
theme: settings.navTheme,
layout: settings.layout,
auth,
users,
platformLanguage: auth?.currentUser?.membership?.platformLanguage,
data: auth?.currentUser?.membership,
}))(GlobalHeaderRight);
-LoginLayout
import React, { useEffect, useState } from 'react';
import type { ConnectState } from '#/models/connect';
import type { MenuDataItem } from '#ant-design/pro-layout';
import { getMenuData, getPageTitle } from '#ant-design/pro-layout';
import { useIntl, connect } from 'umi';
import type { ConnectProps } from 'umi';
import { Col, Row } from 'antd';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import LoginImage from '../assets/loginImage.png';
import SignUpAdminImage from '../assets/adminSignup.svg';
import styles from './LoginLayout.less';
import LanguageDropdown from '#/components/languageDropdown';
import SignupSideText from '#/components/SignupSideText';
export type UserLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
} & Partial<ConnectProps>;
const LoginLayout: React.FC<UserLayoutProps> = (props) => {
const [layoutImage, setLayoutImage] = useState(LoginImage);
const [updateLang, setUpdateLang] = useState<{ lang: string; updated: boolean }>({
lang: '',
updated: false,
});
const [currentLang, setCurrentLang] = useState<any>('');
useEffect(() => {
if (updateLang.updated) {
setUpdateLang({ ...updateLang, updated: false });
setCurrentLang(updateLang.lang);
}
}, [updateLang]);
useEffect(() => {
if (window.location.pathname === '/user/adminSignup/step1') {
setLayoutImage(SignUpAdminImage);
} else if (window.location.pathname === '/user/adminSignup/step2') {
setLayoutImage('TextSignup');
} else setLayoutImage(LoginImage);
}, [window.location.pathname]);
const {
route = {
routes: [],
},
} = props;
const { routes = [] } = route;
const {
children,
location = {
pathname: '',
},
} = props;
const { formatMessage } = useIntl();
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
formatMessage,
breadcrumb,
...props,
});
return (
<>
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<Col
xl={{ span: 12, order: 1 }}
xs={{ span: 0, order: 2 }}
md={{ span: 0, order: 2 }}
lg={{ span: 0, order: 2 }}
style={{ backgroundColor: '#00bfa5' }}
>
{layoutImage === 'TextSignup' ? (
<SignupSideText />
) : (
<img alt="logo" width="100%" height="100%" src={layoutImage} />
)}
</Col>
<Col
xl={{ span: 12, order: 2 }}
lg={{ span: 24, order: 2 }}
sm={{ span: 24, order: 1 }}
xs={{ span: 24, order: 1 }}
>
{' '}
<Row justify="end" className="languageRow">
<LanguageDropdown currentLang={currentLang} setUpdateLang={setUpdateLang} />
</Row>
{children}
</Col>
</div>
</HelmetProvider>
</>
);
};
export default connect(({ settings }: ConnectState) => ({ ...settings }))(LoginLayout);
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;
I have an issue in my React form. I must use the context to know what the name of the form is to set/get the value from the Redux store.
However, I have an issue. My form is in two parts. I set the values in the Redux store and if I need to go back to the previous part of the form, I still have the value saved. However, I have a little problem. I can't set the default state of the form input using the context since I don't know how to access the context in the constructor.
Could you help me achieve this?
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import { handleChange } from 'redux/actions';
import { connect } from 'react-redux';
import FormContext from 'context/FormContext';
export class TextInput extends Component {
constructor (props, context) {
super(props, context);
this.state = { value: this.getValue(context) || '' };
this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this);
}
getRequired () {
if (this.props.required === true) {
return <span className="tw-font-semibold tw-text-red-500 tw-text-sm tw-ml-2">{this.props.t('required')}</span>;
}
}
handleChange (e) {
var value = e.target.value;
this.setState({ value: value });
}
handleBlur (context) {
this.props.handleChange(this.props.name, this.state.value, context.name);
}
getValue (context) {
if (this.props.input && this.props.input[context.name] && this.props.input[context.name][this.props.name]) {
return this.props.input[context.name][this.props.name];
} else {
return undefined;
}
}
render () {
return (
<FormContext.Consumer>
{context =>
<div className={`tw-flex tw-flex-col ${this.props.size} tw-px-2 tw-mb-3`}>
<label htmlFor={this.props.name} className="tw-text-sm tw-font-bold">{this.props.title || this.props.t('common:' + this.props.name)}{this.getRequired()}</label>
<input
value={this.state.value}
onChange={this.handleChange}
onBlur={() => {
this.handleBlur(context);
}}
type={this.props.type} id={this.props.name} placeholder={this.props.title} className="focus:tw-outline-none focus:tw-shadow-outline tw-bg-gray-300 tw-rounded-lg tw-py-2 tw-px-3" />
{this.props.errors && this.props.errors[context.name] && this.props.errors[context.name][this.props.name] && (
<div className="tw-bg-red-100 tw-mt-2 tw-border-l-4 tw-border-red-500 tw-text-red-700 tw-p-2 tw-text-sm">
<p>{this.props.errors[context.name][this.props.name]}</p>
</div>
)}
</div>
}
</FormContext.Consumer>
);
}
}
TextInput.defaultProps = {
size: 'w-full',
required: true,
type: 'text'
};
TextInput.propTypes = {
name: PropTypes.string.isRequired,
title: PropTypes.string,
size: PropTypes.string.isRequired,
required: PropTypes.bool,
type: PropTypes.string,
t: PropTypes.func.isRequired
};
const mapStateToProps = ({ errors, input }, ownProps) => {
return {
errors: errors,
input: input
};
};
export default connect(mapStateToProps, { handleChange })(withTranslation(['input'])(TextInput));
How about you wrap your FormContext wherever you call your TextInput. In that way, you could access your FormContext in your constructor.
function FormThatUsesTextInput() {
return (
<FormContext.Consumer>
{context => <TextInput context={context} {...otherProps} />}
</FormContext.Consumer>
)
}
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
I'm encountering this strange issue that I can figure out why is happing.
This should not be happening since the prop passed down to the History component has not been updated.
./components/History.js
...
const History = ({ previousLevels }) => {
return (
<ScrollView style={styles.container}>
{previousLevels.reverse().map(({ date, stressValue, tirednessValue }) => {
return (
<CardKBT
key={date}
date={date}
stressValue={stressValue}
tirednessValue={tirednessValue}
/>
)
})}
</ScrollView>
)
}
...
export default History
As can be seen in this code (below), the prop to the History is only updated once the user press Save.
App.js
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { AppLoading, Font } from 'expo'
import Store from 'react-native-simple-store'
import { debounce } from 'lodash'
import CurrentLevels from './components/CurrentLevels'
import History from './components/History'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false,
currentLevels: {
stressValue: 1,
tirednessValue: 1,
},
previousLevels: [],
}
this.debounceUpdateStressValue = debounce(this.onChangeStressValue, 50)
this.debounceUpdateTirednessValue = debounce(
this.onChangeTirednessValue,
50
)
}
async componentDidMount() {
const previousLevels = await Store.get('previousLevels')
if (previousLevels) {
this.setState({ previousLevels })
}
}
render() {
const { stressValue, tirednessValue } = this.state.currentLevels
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
...
/>
)
} else {
return (
<View style={{ flex: 1 }}>
<CurrentLevels
stressValue={stressValue}
onChangeStressValue={this.debounceUpdateStressValue}
tirednessValue={tirednessValue}
onChangeTirednessValue={this.debounceUpdateTirednessValue}
onSave={this.onSave}
/>
<History previousLevels={this.state.previousLevels} />
</View>
)
}
}
...
onChangeStressValue = stressValue => {
const { tirednessValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onChangeTirednessValue = tirednessValue => {
const { stressValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onSave = () => {
Store.push('previousLevels', {
date: `${new Date()}`,
...this.state.currentLevels,
}).then(() => {
Store.get('previousLevels').then(previousLevels => {
this.setState({
currentLevels: { stressValue: 1, tirednessValue: 1 },
previousLevels,
})
})
})
}
}
The component will re-render when one of the props or state changes, try using PureComponent or implement shouldComponentUpdate() and handle decide when to re-render.
Keep in mind, PureComponent does shallow object comparison, which means, if your props have nested object structure. It won't work as expected. So your component will re-render if the nested property changes.
In that case, you can have a normal Component and implement the shouldComponentUpdate() where you can tell React to re-render based on comparing the nested properties changes.