I'm trying to refactor a class into a stateless component using React hooks.
The component itself is very simple and I don't see where I'm making a mistake, as it's almost a copy paste from the react docs.
The component is showing a popup when the user clicks on a button (button is passed through props to my component). I'm using typescript.
I commented the line that fails to do what I want in the hooks version
Here's my original class:
export interface NodeMenuProps extends PropsNodeButton {
title?: string
content?: JSX.Element
button?: JSX.Element
}
export interface NodeMenuState {
visible: boolean
}
export class NodeMenu extends React.Component<NodeMenuProps, NodeMenuState> {
state = {
visible: false
}
hide = () => {
this.setState({
visible: false
})
}
handleVisibleChange = (visible: boolean) => {
this.setState({ visible })
}
render() {
return (
<div className={this.props.className}>
<div className={styles.requestNodeMenuIcon}>
<Popover
content={this.props.content}
title={this.props.title}
trigger="click"
placement="bottom"
visible={this.state.visible}
onVisibleChange={this.handleVisibleChange}
>
{this.props.button}
</Popover>
</div>
</div>
)
}
}
Here's the React hooks version:
export interface NodeMenuProps extends PropsNodeButton {
title?: string
content?: JSX.Element
button?: JSX.Element
}
export const NodeMenu: React.SFC<NodeMenuProps> = props => {
const [isVisible, setIsVisible] = useState(false)
const hide = () => {
setIsVisible(false)
}
const handleVisibleChange = (visible: boolean) => {
console.log(visible) // visible is `true` when user clicks. It works
setIsVisible(visible) // This does not set isVisible to `true`.
console.log(isVisible) // is always `false` despite `visible` being true.
}
return (
<div className={props.className}>
<div className={styles.requestNodeMenuIcon}>
<Popover
content={props.content}
title={props.title}
trigger="click"
placement="bottom"
visible={isVisible}
onVisibleChange={handleVisibleChange}
>
{props.button}
</Popover>
</div>
</div>
)
}
Much like setState, the state update behaviour using hooks will also require a re-render and update and hence the change will not be immedialtely visible. If however you try to log state outside of the handleVisibleChange method, you will see the update state
export const NodeMenu: React.SFC<NodeMenuProps> = props => {
const [isVisible, setIsVisible] = useState(false)
const hide = () => {
setIsVisible(false)
}
const handleVisibleChange = (visible: boolean) => {
console.log(visible) // visible is `true` when user clicks. It works
setIsVisible(visible) // This does not set isVisible to `true`.
}
console.log({ isVisible });
return (
<div className={props.className}>
<div className={styles.requestNodeMenuIcon}>
<Popover
content={props.content}
title={props.title}
trigger="click"
placement="bottom"
visible={isVisible}
onVisibleChange={handleVisibleChange}
>
{props.button}
</Popover>
</div>
</div>
)
}
Any action that you need to take on the basis of whether the state was update can be done using the useEffect hook like
useEffect(() => {
// take action when isVisible Changed
}, [isVisible])
Related
Let's say I have a component tree as follows
<App>
</Header>
<Content>
<SelectableGroup>
...items
</SelectableGroup>
</Content>
<Footer />
</App>
Where SelectableGroup is able to select/unselect items it contains by mouse. I'm storing the current selection (an array of selected items) in a redux store so all components within my App can read it.
The Content component has set a ref to the SelectableGroup which enables me to clear the selection programatically (calling clearSelection()). Something like this:
class Content extends React.Component {
constructor(props) {
super(props);
this.selectableGroupRef = React.createRef();
}
clearSelection() {
this.selectableGroupRef.current.clearSelection();
}
render() {
return (
<SelectableGroup ref={this.selectableGroupRef}>
{items}
</SelectableGroup>
);
}
...
}
function mapStateToProps(state) {
...
}
function mapDispatchToProps(dispatch) {
...
}
export default connect(mapStateToProps, mapDispatchToProps)(Content);
I can easily imagine to pass this clearSelection() down to Contents children. But how, and that is my question, can I call clearSelection() from the sibling component Footer?
Should I dispatch an action from Footer and set some kind of "request to call clear selection" state to the Redux store? React to this in the componentDidUpdate() callback in Content and then immediately dispatch another action to reset this "request to call clear selection" state?
Or is there any preferred way to call functions of siblings?
You can use ref to access the whole functions of Content component like so
const { Component } = React;
const { render } = ReactDOM;
class App extends Component {
render() {
return (
<div>
<Content ref={instance => { this.content = instance; }} />
<Footer clear={() => this.content.clearSelection() } />
</div>
);
}
}
class Content extends Component {
clearSelection = () => {
alert('cleared!');
}
render() {
return (
<h1>Content</h1>
);
}
}
class Footer extends Component {
render() {
return (
<div>Footer <button onClick={() => this.props.clear()}>Clear</button>
</div>
);
}
}
render(
<App />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I think the context API would come handy in this situation. I started using it a lot for cases where using the global state/redux didn't seem right or when you are passing props down through multiple levels in your component tree.
Working sample:
import React, { Component } from 'react'
export const Context = React.createContext()
//***************************//
class Main extends Component {
callback(fn) {
fn()
}
render() {
return (
<div>
<Context.Provider value={{ callback: this.callback }}>
<Content/>
<Footer/>
</Context.Provider>
</div>
);
}
}
export default Main
//***************************//
class Content extends Component {
render() {
return (
<Context.Consumer>
{(value) => (
<div onClick={() => value.callback(() => console.log('Triggered from content'))}>Content: Click Me</div>
)}
</Context.Consumer>
)
}
}
//***************************//
class Footer extends Component {
render() {
return (
<Context.Consumer>
{(value) => (
<div onClick={() => value.callback(() => console.log('Triggered from footer'))}>Footer: Click Me</div>
)}
</Context.Consumer>
)
}
}
//***************************//
Assuming content and footer and in there own files (content.js/footer.js) remember to import Context from main.js
According to the answer of Liam , in function component version:
export default function App() {
const a_comp = useRef(null);
return (
<div>
<B_called_by_a ref={a_comp} />
<A_callB
callB={() => {
a_comp.current.f();
}}
/>
</div>
);
}
const B_called_by_a = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
f() {
alert("cleared!");
}
}));
return <h1>B. my borther, a, call me</h1>;
});
function A_callB(props) {
return (
<div> A I call to my brother B in the button
<button onClick={() => { console.log(props); props.callB();}}>Clear </button>
</div>
);
}
you can check it in codesandbox
One way I use to call the sibling function is to set a new date.
Let me explain more:
In their parent we have a function that set new date in a state (the state's name is something like "refresh date" or "timestamp" or something similar).
And you can pass state to sibling by props and in sibling component you can use useEffect for functional components or componentDidUpdate for class components and check when the date has changed, call your function .
However you can pass the new date in redux and use redux to check the date
const Parent = () => {
const [refreshDate, setRefreshDate] = useState(null);
const componentAClicked = () => setRefreshDate(new Date())
return (
<>
<ComponentA componentAClicked={componentAClicked}/>
<ComponentB refreshDate={refreshDate}/>
</>
}
const ComponentA = ({ componentAClicked}) => {
return (
<button onClick={componentAClicked}>click to call sibling function!!<button/>
)
}
const ComponentB = ({ refreshDate }) => {
useEffect(()=>{
functionCalledFromComponentA()
},[refreshDate]
const functionCalledFromComponentA = () => console.log("function Called")
return null
}
Functional components & TypeScript
Note 1: I've swapped useRef for createRef.
Note 2: You can insert the component's prop type in the second type parameter here: forwardRef<B_fns, MyPropsType>. It's confusing because the props & ref order are reversed.
type B_fns = {
my_fn(): void;
}
export default function App() {
const a_comp = createRef<B_fns>();
return (
<div>
<B_called_by_a ref={a_comp} />
<A_callB
callB={() => {
a_comp.current?.my_fn();
}}
/>
</div>
);
}
const B_called_by_a = forwardRef<B_fns>((props, ref) => {
useImperativeHandle(ref, () => ({
my_fn() {
alert("cleared!");
}
}));
return <h1>B. my borther, a, call me</h1>;
});
function A_callB(props) {
return (
<div> A I call to my brother B in the button
<button onClick={() => { console.log(props); props.callB();}}>Clear </button>
</div>
);
}
Below is the HOC and it is connected to redux store too. The WrappedComponent function is not fetching the redux state on change of storedata. What could be wrong here?
export function withCreateHOC<ChildProps>(
ChildComponent: ComponentType,
options: WithCreateButtonHOCOptions = {
title: 'Create',
},
) {
function WrappedComponent(props: any) {
const { createComponent, title } = options;
const [isOpen, setisOpen] = useState(false);
function onCreateClick() {
setisOpen(!isOpen);
Util.prevDefault(() => setisOpen(isOpen));
}
return (
<div>
<ChildComponent {...props} />
<div>
<Component.Button
key={'add'}
big={true}
round={true}
primary={true}
onClick={Util.prevDefault(onCreateClick)}
className={'float-right'}
tooltip={title}
>
<Component.Icon material={'add'} />
</Component.Button>
</div>
<OpenDrawerWithClose
open={isOpen}
title={title}
setisOpen={setisOpen}
createComponent={createComponent}
/>
</div>
);
}
function mapStateToProps(state: any) {
console.log('HOC mapStateToProps isOpen', state.isOpen);
return {
isOpen: state.isOpen,
};
}
// Redux connected;
return connect(mapStateToProps, {})(WrappedComponent);
}
Expecting isOpen to be used from ReduxStore and update the same with WrappedComponent here. By any chance this should be changed to class component?
The above HOC is used as:
export const Page = withCreateHOC(
PageItems,
{
createComponent: <SomeOtherComponent />,
title: 'Create',
},
);
Overview
You don't want isOpen to be a local state in WrappedComponent. The whole point of this HOC is to access isOpen from your redux store. Note that nowhere in this code are you changing the value of your redux state. You want to ditch the local state, access isOpen from redux, and dispatch an action to change isOpen in redux.
Additionally we've got to replace some of those anys with actual types!
It seems a little suspect to me that you are passing a resolved JSX element rather than a callable component as createComponent (<SomeOtherComponent /> vs SomeOtherComponent), but whether that is correct or a mistake depends on what's in your OpenDrawerWithClose component. I'm going to assume it's correct as written here.
There's nothing technically wrong with using connect, but it feels kinda weird to use an HOC inside of an HOC so I am going to use the hooks useSelector and useDispatch instead.
Step By Step
We want to create a function that takes a component ComponentType<ChildProps> and some options WithCreateButtonHOCOptions. You are providing a default value for options.title so we can make it optional. Is options.createComponent optional or required?
interface WithCreateButtonHOCOptions {
title: string;
createComponent: React.ReactNode;
}
function withCreateHOC<ChildProps>(
ChildComponent: ComponentType<ChildProps>,
options: Partial<WithCreateButtonHOCOptions>
) {
We return a function that takes the same props, but without isOpen or toggleOpen, if those were properties of ChildProps.
return function (props: Omit<ChildProps, 'isOpen' | 'toggleOpen'>) {
We need to set defaults for the options in the destructuring step in order to set only one property.
const { createComponent, title = 'Create' } = options;
We access isOpen from the redux state.
const isOpen = useSelector((state: { isOpen: boolean }) => state.isOpen);
We create a callback that dispatches an action to redux -- you will need to handle this in your reducer. I am dispatching a raw action object {type: 'TOGGLE_OPEN'}, but you could make an action creator function for this.
const dispatch = useDispatch();
const toggleOpen = () => {
dispatch({type: 'TOGGLE_OPEN'});
}
We will pass these two values isOpen and toggleOpen as props to ChildComponent just in case it want to use them. But more importantly, we can use them as click handlers on your button and drawer components. (Note: it looks like drawer wants a prop setIsOpen that takes a boolean, so you may need to tweak this a bit. If the drawer is only shown when isOpen is true then just toggling should be fine).
Code
function withCreateHOC<ChildProps>(
ChildComponent: ComponentType<ChildProps>,
options: Partial<WithCreateButtonHOCOptions>
) {
return function (props: Omit<ChildProps, 'isOpen' | 'toggleOpen'>) {
const { createComponent, title = 'Create' } = options;
const isOpen = useSelector((state: { isOpen: boolean }) => state.isOpen);
const dispatch = useDispatch();
const toggleOpen = () => {
dispatch({ type: 'TOGGLE_OPEN' });
}
return (
<div>
<ChildComponent
{...props as ChildProps}
toggleOpen={toggleOpen}
isOpen={isOpen}
/>
<div>
<Component.Button
key={'add'}
big={true}
round={true}
primary={true}
onClick={toggleOpen}
className={'float-right'}
tooltip={title}
>
<Component.Icon material={'add'} />
</Component.Button>
</div>
<OpenDrawerWithClose
open={isOpen}
title={title}
setisOpen={toggleOpen}
createComponent={createComponent}
/>
</div>
);
}
}
This version is slightly better because it does not have the as ChildProps assertion. I don't want to get too sidetracked into the "why" but basically we need to insist that if ChildProps takes an isOpen or toggleOpen prop, that those props must have the same types as the ones that we are providing.
interface AddedProps {
isOpen: boolean;
toggleOpen: () => void;
}
function withCreateHOC<ChildProps>(
ChildComponent: ComponentType<Omit<ChildProps, keyof AddedProps> & AddedProps>,
options: Partial<WithCreateButtonHOCOptions>
) {
return function (props: Omit<ChildProps, keyof AddedProps>) {
const { createComponent, title = 'Create' } = options;
const isOpen = useSelector((state: { isOpen: boolean }) => state.isOpen);
const dispatch = useDispatch();
const toggleOpen = () => {
dispatch({ type: 'TOGGLE_OPEN' });
}
return (
<div>
<ChildComponent
{...props}
toggleOpen={toggleOpen}
isOpen={isOpen}
/>
<div>
<Component.Button
key={'add'}
big={true}
round={true}
primary={true}
onClick={toggleOpen}
className={'float-right'}
tooltip={title}
>
<Component.Icon material={'add'} />
</Component.Button>
</div>
<OpenDrawerWithClose
open={isOpen}
title={title}
setisOpen={toggleOpen}
createComponent={createComponent}
/>
</div>
);
}
}
Playground Link
I have a React HOC that hides a flyover/popup/dropdown, whenever I click outside the referenced component. It works perfectly fine when using local state. My HOC looks like this:
export default function withClickOutside(WrappedComponent) {
const Component = props => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const handleClickOutside = event => {
if (ref?.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => setOpen(false);
}, [ref]);
return <WrappedComponent open={open} setOpen={setOpen} ref={ref} {...props} />;
};
return Component;
}
When in use I just wrap up the required component with the HOC function
const TestComponent = () => {
const ref = useRef();
return <Wrapper ref={ref} />;
}
export default withClickOutside(TestComponent);
But I have some flyover containers that are managed from Redux when they are shown, or when they are hidden. When the flyover is shown, I want to have the same behavior, by clicking outside the referenced component to hide it right away. Here's a example of a flyover:
const { leftFlyoverOpen } = useSelector(({ toggles }) => toggles);
return (
<div>
<Wrapper>
<LeftFlyoverToggle
onClick={() => dispatch({ type: 'LEFT_FLYOVER_OPEN' })}
>
...
</Wrapper>
{leftFlyoverOpen && <LeftFlyover />}
{rightFlyoverOpen && <RightFlyover />}
</div>
);
Flyover component looks pretty straightforward:
const LefFlyover = () => {
return <div>...</div>;
};
export default LefFlyover;
Question: How can I modify the above HOC to handle Redux based flyovers/popup/dropdown?
Ideally I would like to handle both ways in one HOC, but it's fine if the examples will be only for Redux solution
You have a few options here. Personally, I don't like to use HOC's anymore. Especially in combination with functional components.
One possible solution would be to create a generic useOnClickOutside hook which accepts a callback. This enables you to dispatch an action by using the useDispatch hook inside the component.
export default function useOnClickOutside(callback) {
const [element, setElement] = useState(null);
useEffect(() => {
const handleClickOutside = event => {
if (element && !element.contains(event.target)) {
callback();
}
};
if (element) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [element, callback]);
return setElement;
}
function LeftFlyOver() {
const { leftFlyoverOpen } = useSelector(({ toggles }) => toggles);
const dispatch = useDispatch();
const setElement = useOnClickOutside(() => {
dispatch({ type: 'LEFT_FLYOVER_CLOSE' });
});
return (
<Dialog open={leftFlyoverOpen} ref={ref => setElement(ref)}>
...
</Dialog>
)
}
i am toggling state of a dialog using context. initially the state isOpen is set to false. when add button is clicked the state isOpen is true and clicking cancel button will set state isOpen to false.
now when user doesnt click cancel button then the state isOpen is true still even when user navigates to another page.
below is my code,
function Main() {
return(
<DialogContextProvider>
<Parent/>
</DialogContextProvider>
);
}
function Parent() {
return (
<AddButton/>
);
}
function AddButton() {
const { isOpen, toggleDialog } = useDialogs();
return(
<Icon
onClick={() => {
toggleDialog(true); //isOpen set to true
}}/>
{isOpen &&
<Dialog
onCancel={() => {
toggleDialog(false); //isOpen set to false
}}
);
}
interface DialogsState {
isOpen: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialState: DialogsState = {
isOpen: false,
setIsOpen: () => {},
};
const DialogsContext = React.createContext<DialogsState>(
initialState
);
export const DialogsContextProvider: React.FC = ({ children }) => {
const [isOpen, setOpen] = React.useState<boolean>(false);
return (
<DialogsContext.Provider
value={{isOpen,setOpen}}>
{children}
</DialogsContext.Provider>
);
};
export function useDialogs() {
const {
isOpen,
setOpen,
} = React.useContext(ScheduleDialogsContext);
const toggleDialog = (toggleValue: boolean) => {
setOpen(toggleValue);
};
return {
isOpen,
toggleDialog,
};
}
I am not sure how to set the state isOpen to false when this dialog unmounts meaning when user opens this dialog the isOpen state is true. if user doesnt click cancel on dialog and moves to another page still this isOpen state is true. it should be false in that case.
how can i fix this. could someone help me with this? thanks.
you can use useEffect and return a function from it, this fn will be called when the component is unmounted
e.g.
useEffect(() => {
// called on mount
return () => {
// called on unmount
}
}, [])
Here is a link for reference https://reactjs.org/docs/hooks-effect.html
useEffect hook has return which fires when component unmount.
export const DialogsContextProvider: React.FC = ({ children }) => {
const [isOpen, setOpen] = React.useState<boolean>(false);
useEffect(() => {
return () => {
setOpen(false);
}
}, []);
return (
<DialogsContext.Provider
value={{isOpen,setOpen}}>
{children}
</DialogsContext.Provider>
);
};
I'm digging into my first react/redux application and I've been having quite a bit of trouble mapping my dispatch actions to onClick events in my components.
I've tried a couple of variations of trying to bind the onClick Event to the dispatch, but I always end up with either :
ReferenceError: onMovieClick is not defined
or alternatively when I do end up binding a function correctly I'll get an error related to dispatch is not defined.
My Goal
I'm trying to implement a filter(delete) from store function
actions/movieActions.js
import * as actionTypes from './actionTypes'
export const createMovie = (movie) => {
return {
type: actionTypes.CREATE_MOVIE,
movie
}
};
export const deleteMovie = (id) => {
console.log('action triggered. movie index:' + id)
return {
type: actionTypes.DELETE_MOVIE,
id
}
}
reducers/movieReducers.js
export default (state = [], action) => {
switch (action.type){
case 'CREATE_MOVIE':
return [
...state,
Object.assign({}, action.movie)
];
case 'DELETE_MOVIE':
return [
state.filter(({ id }) => id !== action.id)
]
default:
return state;
}
};
components/MovieList.js
import React from 'react'
import Slider from 'react-slick'
import { dispatch, connect } from 'react-redux'
import {Icon} from 'react-fa'
import { deleteMovie } from '../../actions/movieActions'
import 'slick-carousel/slick/slick.css'
import 'slick-carousel/slick/slick-theme.css'
import './MovieList.scss'
class MovieList extends React.Component{
constructor(props){
super (props)
}
handleClick(id) {
dispatch(deleteMovie(id))
}
onMovieClick(id){
dispatch.deleteMovie(id)
}
render () {
// Settings for slick-carousel
let settings = {
infinite: true,
speed: 500
}
return (
<div className='col-lg-12'>
{this.props.movies.map((b, i) =>
<div key={i} className="col-lg-2">
<Slider {...settings}>
{b.images.map((b, z) =>
<div className="img-wrapper">
<Icon name="trash" className="trash-icon" onClick={() =>
console.log(this.props.movies[i].id),
onMovieClick(this.props.movies[i].id)
}/>
<img className="img-responsive" key={z} src={b.base64}></img>
</div>
)}
</Slider>
<div className="text-left info">
<h2>{b.title}</h2>
<p>{b.genre}</p>
</div>
</div>
)}
</div>
)
}
}
// map state from store to props
const mapStateToProps = (state) => {
return {
movies: state.movies
}
};
// Map actions to props
const mapDispatchToProps = (dispatch) => {
return {
onMovieClick: (id) => {
dispatch(deleteMovie(id))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MovieList)
Would love some advice if anyone has a moment.
Since you are passing onMovieClick through connect, you can actually invoke it from the MovieList component props. First, I would remove the onMovieClick method definition in your MovieList component and then use this.props.onMovieClick in the onclick handler of Icon like so:
<Icon name="trash" className="trash-icon" onClick={() =>
console.log(this.props.movies[i].id),
this.props.onMovieClick(this.props.movies[i].id)
}/>