How to call functions in composition react-hooks? - reactjs

I have one base hook(baseHook.js) which has some functions in it. Using composition I am trying to call those functions in child hook(formHook.js).
baseHook.js is as follow
import React, { Fragment, useEffect, useState} from "react";
import PropTypes from "prop-types";
const BaseHook = ({ ...props }) => {
const [show, setshow] = useState(false);
//when hovered on activeInput -->'activeInput' is declared but its value is never read.
const activeInput = (input) => {
setshow(true);
}
return (
<Fragment>
{props.children}
{show ? <div>
<p>Div is visible</p>
</div> : null}
</Fragment>
);
};
BaseHook.propTypes = {
activeInput:PropTypes.func,
};
export default BaseHook;
Now I am trying to use baseHook.js in another formHook.js where onFocus of input activeInput should get called.
import React, { Fragment, useEffect, useState} from "react";
import BaseHook from "components/BaseHook";
const FormHook = ({ ...props }) => {
return (
<BaseHook>
<Fragment>
<input
title= {"Input"}
onFocus={() => activeInput(InputValue)}
value={InputValue}
className="required-field"
/>
</Fragment>
<BaseHook>
);
};
export default FormHook;
activeInput function is not getting called from baseHook hence not able to setshow(true).
I am able to achieve this with react-class components using inheritance but is there way to call functions in composition in react-hooks?

Related

useRef() returning 'undefined' when used with custom hook on initial render when using filter()

I have an image slider component and a simple custom hook that gets the refElement and the width of the element using the useRef hook. -
The code sandbox is here Image Slider
When I use the slider component and just map the data in without filtering, everything works fine. If I filter and map the data then I get Uncaught TypeError: elementRef.current is undefined . (In the sandbox you have to comment out the second instance (unfiltered) of SliderTwo to recreate the error. Why does it work without the filter but not with (when rendered by itself)? More in depth explanation below.
useSizeElement()
import { useState, useRef, useEffect } from 'react';
const useSizeElement = () => {
const [width, setWidth] = useState(0);
const elementRef = useRef();
useEffect(() => {
setWidth(elementRef.current.clientWidth); // This will give us the width of the element
}, [elementRef.current]);
return { width, elementRef };
};
export default useSizeElement;
I call the hook (useSizeElement) inside of a context because I need the width to use in another hook in a different component thus:
context
import React, { createContext, useState, useEffect} from 'react';
import useSizeElement from '../components/flix-slider/useSizeElement';
export const SliderContext = createContext();
export const SliderProvider = ({children}) => {
const { width, elementRef } = useSizeElement();
const [currentSlide, setCurrentSlide] = useState();
const [isOpen, setIsOpen] = useState(false)
console.log('context - width', width, 'elementRef', elementRef)
const showDetailsHandler = movie => {
setCurrentSlide(movie);
setIsOpen(true)
};
const closeDetailsHandler = () => {
setCurrentSlide(null);
setIsOpen(false)
};
const value = {
onShowDetails: showDetailsHandler,
onHideDetails: closeDetailsHandler,
elementRef,
currentSlide,
width,
isOpen
};
return <SliderContext.Provider value={value}>{children}</SliderContext.Provider>
}
I get the width of the component from the elementRef that was passed from the context.-
Item Component
import React, { Fragment, useContext } from 'react';
import { SliderContext } from '../../store/SliderContext.context';
import ShowDetailsButton from './ShowDetailsButton';
import Mark from './Mark';
import { ItemContainer } from './item.styles';
const Item = ({ show }) => {
const { onShowDetails, currentSlide, isOpen, elementRef } =
useContext(SliderContext);
const isActive = currentSlide && currentSlide.id === show.id;
return (
<Fragment>
<ItemContainer
className={isOpen ? 'open' : null}
ref={elementRef}
isActive={isActive}
isOpen={isOpen}
>
<img
src={show.thumbnail.regular.medium}
alt={`Show title: ${show.title}`}
/>
<ShowDetailsButton onClick={() => onShowDetails(show)} />
</ItemContainer>
</Fragment>
);
};
export default Item;
The width is passed using context where another hook is called in the Slider Component:
Slide Component
import useSizeElement from './useSizeElement';
import { OuterContainer } from './SliderTwo.styles';
const SliderTwo = ({ children }) => {
const {currentSlide, onHideDetails, isOpen, width, elementRef } = useContext(SliderContext);
const { handlePrev, handleNext, slideProps, containerRef, hasNext, hasPrev } =
useSliding( width, React.Children.count(children));
return (
<Fragment>
<SliderWrapper>
<OuterContainer isOpen={isOpen}>
<div ref={containerRef} {...slideProps}>
{children}
</div>
</OuterContainer>
{hasPrev && <SlideButton showLeft={hasPrev} onClick={handlePrev} type="prev" />}
{hasNext && <SlideButton showRight={hasNext} onClick={handleNext} type="next" />}
</SliderWrapper>
{currentSlide && <Content show={currentSlide} onClose={onHideDetails} />}
</Fragment>
);
};
export default SliderTwo;
Now everything works fine if I just map the data with no filters into the slider as shown in the sandbox. But if I apply a filter to display only what I want I get -
Uncaught TypeError: elementRef.current is undefined
I do know that you can't create a ref on an element that does not yet exist and I've seen examples where you can use useEffect to get around it but I can't find the solution to get it to work.
Here is the App.js - To see the error I'm getting, comment out the second instance of . As long as I'm running one instance without filtering the data, it works, but it won't work by itself.
import { useState, useEffect, Fragment } from "react";
import SliderTwo from "./components/SliderTwo";
import Item from "./components/Item";
import shows from "./data.json";
import "./App.css";
function App() {
const [data, setData] = useState(null);
const datafunc = () => {
let filteredData = shows.filter((show) => {
if (show.isTrending === true) {
return show;
}
});
setData(filteredData);
};
useEffect(() => {
datafunc();
}, []);
console.log("Trending movies", data);
return (
<Fragment>
<div className="testDiv">
{shows && data && (
<SliderTwo>
{data && data.map((show) => <Item show={show} key={show.id} />)}
</SliderTwo>
)}
</div>
<div className="testDiv">
<SliderTwo>
{shows.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div>
</Fragment>
);
}
export default App;
Full code: Sandbox - https://codesandbox.io/s/twilight-sound-xqglgk
I think it may be an issue when the useSizeElement is first mounted as the useEffect will run once at the beginning of each render.
When it runs at the first instance and the ref is not yet defined so it was returning the error: Cannot read properties of undefined (reading 'clientWidth')
If you modify your code to this I believe it should work:
import { useState, useRef, useEffect } from "react";
const useSizeElement = () => {
const [width, setWidth] = useState(0);
const elementRef = useRef();
useEffect(() => {
if (elementRef.current) setWidth(elementRef.current.clientWidth); //
This will give us the width of the element
}, [elementRef]);
return { width, elementRef };
};
export default useSizeElement;
This way you are checking if the elementRef is defined first before setting the width
UPDATE:
<Fragment>
<div className="testDiv">
<SliderTwo>
{shows
.filter((show) => {
if (show.isTrending === true) {
return show;
}
return false;
})
.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div>
{/* <div className="testDiv">
<SliderTwo>
{shows.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div> */}
</Fragment>

React performance optimization

Instead of using useMemo, I simply define the "person" object outside of the component. which fix the issue of unnecessary rerender for reference changing of props
App.js
import React, { useState } from "react";
import Child from "./Child";
let person = { fname: "mahabub", lname: "shaon" };
export default function App() {
const [state, setState] = useState(0);
console.log("rendering parent");
return (
<>
<button onClick={() => setState(state + 1)}>click to render</button>
<Child person={person} />
</>
);
}
Child.js
import React from "react";
function Child({ person }) {
console.log("rendering child");
return (
<h1>
{person.fname} {person.lname}
</h1>
);
}
export default React.memo(Child);
Is it ok or any drawbacks of this approach?

How to get ref on children using react hook?

I'm trying to get hold of ref on children component but it doesn't seem to be working. The same approach works fine with the React class component but not with hooks.
import React, { useState, useRef } from "react";
export default function TestContainer(props) {
const ref = useRef(null);
return (
<div className="test-container" onClick={() => console.log(ref) // this logs null always}>
{React.Children.map(props.children, c =>
React.cloneElement(c, {
ref: n => {
console.log(n);
ref.current = n;
},
className: "test-container"
})
)}
</div>
);
}
export function Test(props) {
return <div className="test" {...props}>
{props.children}
</div>
}
Your component is okay. It is probably because the are no children rendered to that component. I reproduced it with using TestContainer in App and put <h2>Ref</h2> as a child of TestContainer:
(removed the comment of course, since it has been hiding the } )
App.js
import React from "react";
import "./styles.css";
import TestContainer from "./TestContainer";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<TestContainer>
<h2>Ref</h2>
</TestContainer>
</div>
);
}
TestContainer.js
import React, { useState, useRef } from "react";
export default function TestContainer(props) {
const ref = useRef(null);
return (
<div className="test-container" onClick={() => console.log(ref)}>
{React.Children.map(props.children, c =>
React.cloneElement(c, {
ref: n => {
console.log(n);
ref.current = n;
},
className: "test-container"
})
)}
</div>
);
}
CodeSndbox:
HERE

React: Cannot update a component from inside the function body of a different component

i'm trying to only render the component <IntercomClient /> after a user clicks "Accept" on a cookie consent banner. Clicking accept changes the GlobalLayout's intercomIsActive state to true and thereby renders the IntercomClient. This is working but the warning concerns me.
How can I workaround the child/parent state change? I've been looking around but don't really understand.
import React, { useState } from 'react'
import { CookieBanner } from '#palmabit/react-cookie-law'
import IntercomClient from '../components/intercomClient'
const GlobalLayout = ({ location, children }) => {
const [intercomIsActive, setIntercomIsActive] = useState(false)
return (
...
<CookieBanner
onAccept={() => setIntercomIsActive(true)}
/>
<IntercomClient active={intercomIsActive}/>
...
)}
IntercomClient
import React from 'react';
import Intercom from 'react-intercom'
const IntercomClient = ({ active }) => {
return active ? <div><Intercom appID="XXXXXX" /></div> : null
}
export default IntercomClient;
import React, {useState} from 'react';
const Example = () => {
const [intercomIsActive, setIntercomIsActive] = useState(false)
return (
<Layout>
...
<CookieBanner
onAccept={() => setIntercomIsActive(true)}
/>
<IntercomClient active={intercomIsActive}/>
...
</Layout>
);
};
export default Example;
import React, {useState} from 'react';
const Example = () => {
const [intercomIsActive, setIntercomIsActive] = useState(false)
return (
<Layout>
...
<CookieBanner
onAccept={() => setIntercomIsActive(true)}
/>
{
intercomIsActive &&
<IntercomClient active={intercomIsActive}/>
}
...
</Layout>
);
};
export default Example;

React complains element type is invalid when trying to use context

I'm trying to use React Context to update navbar title dynamically from other child components. I created NavbarContext.js as follows. I have wrapped AdminLayout with NavContext.Provider and use useContext in Course.js to dynamically update navbar title inside useEffect. However, when I'm doing this, react throws the following error on the screen.
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
How can I use context properly so that I can update Header title from Course.js inside its useEffect?
NavbarContext.js
import React, {useState} from 'react'
export default () => {
const [name,setName] = useState("")
const NavContext = React.createContext({
name: "",
changeName: name => setName(name)
})
const NavProvider = NavContext.Provider
const NavConsumer = NavContext.Consumer
return NavContext
}
AdminLayout.js
<NavContext.Provider>
<div className={classes.wrapper}>
<Sidebar
routes={routes}
logoText={"Widubima"}
logo={logo}
image={image}
handleDrawerToggle={handleDrawerToggle}
open={mobileOpen}
color={color}
{...rest}
/>
<div className={classes.mainPanel} ref={mainPanel}>
<Navbar
routes={routes}
handleDrawerToggle={handleDrawerToggle}
{...rest}
/>
{/* On the /maps route we want the map to be on full screen - this is not possible if the content and conatiner classes are present because they have some paddings which would make the map smaller */}
{getRoute() ? (
<div className={classes.content}>
<div className={classes.container}>{switchRoutes}</div>
</div>
) : (
<div className={classes.map}>{switchRoutes}</div>
)}
</div>
</div>
</NavContext.Provider>
Navbar.js
import NavContext from "context/NavbarContext"
export default function Header(props) {
function makeBrand() {
var name;
props.routes.map(prop => {
if (window.location.href.indexOf(prop.layout + prop.path) !== -1) {
name = prop.name;
document.title = name;
}
return null;
});
return name;
}
return (
<AppBar className={classes.appBar + appBarClasses}>
<Toolbar className={classes.container}>
<div className={classes.flex}>
{/* Here we create navbar brand, based on route name */}
<NavContext.Consumer>
{({ name, setName }) => (
<Button
color="transparent"
href="#"
className={classes.title}
style={{ fontSize: "1.5em", marginLeft: "-2%" }}
>
{makeBrand() || name}
</Button>
)}
</NavContext.Consumer>
</Toolbar>
</AppBar>
);
}
Course.js
import React, { useState, useEffect, useContext } from "react";
import NavContext from "context/NavbarContext"
const AdminCourse = props => {
const context = useContext(NavContext);
useEffect(() => {
Axios.get('/courses/'+props.match.params.courseId).then(
res => {
context.changeName("hello")
}
).catch(err => {
console.log(err)
})
return () => {
setCourseId("");
};
});
return (
<GridContainer>
</GridContainer>
);
};
export default AdminCourse;
i think problem is there with your NavbarContext.js.
you are not exporting NavContext also.
you are defining provider, consumer but you are not using them either.
here's how you can solve your problem.
first create context and it's provider in a file as following.
NavContext.js
import React, { useState } from "react";
const NavContext = React.createContext();
const NavProvider = props => {
const [name, setName] = useState("");
let hookObject = {
name: name,
changeName: setName
};
return (
<NavContext.Provider value={hookObject}>
{props.children}
</NavContext.Provider>
);
};
export { NavProvider, NavContext };
in above code first i am creating context with empty value.
the i am creating NavProvider which actually contains value name as a state hook inside it.hookObject exposes state as per your naming conventions in code.
now i for testing purpose i defined two consumers.
one is where we update name in useEffect, that is ,
ConsumerThatUpdates.js
import React, { useContext, useEffect } from "react";
import { NavContext } from "./NavContext";
const ConsumerThatUpdates = () => {
const { changeName } = useContext(NavContext);
useEffect(() => {
changeName("NEW NAME");
}, [changeName]);
return <div>i update on my useeffect</div>;
};
export default ConsumerThatUpdates;
you can update useEffect as per your needs.
another is where we use the name,
ConsumerThatDisplays.js
import React, { useContext } from "react";
import { NavContext } from "./NavContext";
const ConsumerThatDisplays = () => {
const { name } = useContext(NavContext);
return <div>{name}</div>;
};
export default ConsumerThatDisplays;
and finally my App.js looks like this,
App.js
import React from "react";
import "./styles.css";
import { NavProvider } from "./NavContext";
import ConsumerThatDisplays from "./ConsumerThatDisplays";
import ConsumerThatUpdates from "./ConsumerThatUpdates";
export default function App() {
return (
<div className="App">
<NavProvider>
<ConsumerThatDisplays />
<ConsumerThatUpdates />
</NavProvider>
</div>
);
}
hope this helps!!
if you want to know more about how to use context effectively, i recooHow to use React Context effectively

Resources