React scroll start from bottom - reactjs

I have a chat page where I get a message from the server, I need to implement it so that the scroll of the starts from the last element. I use the useRef hook for scrolling, but my console gives an error (TypeError: Cannot read property 'scrollIntoView' of null). but everything works correctly in the sandbox. I can not understand where is my mistake.
codesandbox
React:
import React, {useEffect, useRef} from "react";
import styled from "styled-components";
const TabPanelBody = ({messages}) => {
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
};
useEffect(scrollToBottom, []);
return (
<TabBody>
{
messages.messages ? messages.messages.map((data, index) => {
return (
<Messages key={index} className={ data.my ? 'send-messages' : 'incoming-messages' }>
{data.message}
<div ref={messagesEndRef} />
</Messages>
)
}) : null
}
</TabBody>
);
};
Error

Check if there is a Current value
The error states that messagesEndRef.current is null. So make sure that there's a value inside the current before trying to run the function scrollIntoView.
messagesEndRef.current && messagesEndRef.current.scrollIntoView({ behavior: "smooth" });

The ref isn't set yet when the useEffect is called. You have to check if the ref is set and then call the function.
import React, {useEffect, useRef} from "react";
import styled from "styled-components";
const TabPanelBody = ({messages}) => {
const messagesEndRef = useRef(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messagesEndRef.current]);
return (
<TabBody>
{
messages.messages ? messages.messages.map((data, index) => {
return (
<Messages key={index} className={ data.my ? 'send-messages' : 'incoming-messages' }>
{data.message}
<div ref={messagesEndRef} />
</Messages>
)
}) : null
}
</TabBody>
);
};

Related

In react, how can I deselect an element after it has been set as active with onClick?

I have an image grid in react, with an onclick function that highlights the selected image. Clicking another image will change the active element but I'd like to be able to re-click the selected icon to deselect it and return the grid to default.
Here is my codesandbox and the script is below
import { useState, useRef, useEffect } from "react";
import "./App.css";
import { Data } from "./Data.js";
function App() {
const scrollRef = useRef();
useEffect(() => {
const el = scrollRef.current;
if (el) {
const wheelListener = (e) => {
e.preventDefault();
el.scrollTo({
left: el.scrollLeft + e.deltaY * 5,
behavior: "smooth"
});
};
el.addEventListener("wheel", wheelListener);
return () => el.removeEventListener("wheel", wheelListener);
}
}, []);
const [active, setActive] = useState(-1);
const [active2, setActive2] = useState(false);
return (
<div ref={scrollRef} className="grid_container">
{Data.map((prev, i) => {
return (
<div
onClick={() => {
setActive(i);
setActive2(true);
console.log(prev.Team);
}}
className={`${
(active === i && "scale") || (active2 && "notScale")
} card`}
key={i}
>
<img src={prev.TeamBadge} alt="" />
</div>
);
})}
</div>
);
}
export default App;
If I understand the problem correctly, I think this solves the problem, based on the codesandbox:
onClick={() => {
setActive(i);
if (active === i) {
setActive2(null);
setActive(null);
} else {
setActive2(true);
}
console.log(i);
console.log(prev.Team);
}}
since every team has its unique number, the logic here is to check if the same number is in the state (if that makes sense). Let me know if this was you are looking for!
You can set your state like that :
setActive2(!active2);

TypeError: Cannot read property 'onMonthSelect' of undefined while using "useRef"

i am trying to use useRef in my react functional component, but when i try to access it I am getting error
"TypeError: Cannot read property 'onMonthSelect' of undefined while using "useRef" " .
Here is the code below for this
import React, { useRef, useState, useEffect } from "react";
import moment from "moment";
import "react-dates/initialize";
import "react-dates/lib/css/_datepicker.css";
import { SingleDatePicker } from "react-dates";
const SingleDatePickerComponent = () => {
const monthController = useRef();
const [createdAt, setCreatedAt] = useState(moment());
const onDateChange = (createdAt) => {
console.log(createdAt);
setCreatedAt(createdAt);
};
useEffect(() => {
console.log(monthController);
// TODO: check if month is visible before moving
monthController.current.onMonthSelect(
monthController.current.month,
createdAt.format("M")
);
//In this useEffect i am getting the error
}, [createdAt]);
return (
<div>
<div style={{ marginLeft: "200px" }}>
</div>
<SingleDatePicker
date={createdAt}
startDateId="MyDatePicker"
onDateChange={onDateChange}
renderMonthElement={(...args) => {
// console.log(args)
monthController.current = {
month: args[0].month,
onMonthSelect: args[0].onMonthSelect,
};
// console.log(monthController)
return args[0].month.format("MMMM");
}}
id="SDP"
/>
</div>
);
};
export default SingleDatePickerComponent;
The ref value won't be set yet on the initial render. Use a guard clause or Optional Chaining operator on the access.
useEffect(() => {
// TODO: check if month is visible before moving
monthController.current && monthController.current.onMonthSelect(
monthController.current.month,
createdAt.format("M")
);
}, [createdAt]);
or
useEffect(() => {
// TODO: check if month is visible before moving
monthController.current?.onMonthSelect(
monthController.current.month,
createdAt.format("M")
);
}, [createdAt]);
It may also help to provide a defined initial ref value.
const monthController = useRef({
onMonthSelect: () => {},
});

"Toggle undifined" message when click button in React.js

I am newbie in React js, and I have made a toggle class button in Reactjs.
But When I clicked i got error message.
TypeError : Cannot read property 'toggle' of undefined
This is my code below.
What is wrong with my code? please help.
import React from 'react'
export default function About() {
const navRef = React.useRef(null);
const btnRef = React.useRef(null);
const onToggleClick = () => {
navRef.current.classList.toggle("show");
btnRef.current.classList.toggle("active");
};
return (
<>
<button onClick={onToggleClick} ref={btnRef}>Toggle</button>
<nav ref={navRef}>Navigation menu</nav>
</>
);
}
In react, you can learn to toggle by using state.
In the following example, we set a toggle state show, and toggle it true/false.
We will then set the className with the appropriate css class we want it to have.
import React, { useState, useEffect } from 'react'
export default function About() {
const [ show, setShow ] = useState(false) //default to hide
useEffect(() => {
if (show) {
document.body.classList.add('bodyclass');
} else {
document.body.classList.remove('bodyclass');
}
}, [show])
const onToggleClick = () => setShow(!show) //if it's true, set to false. vice-versa
return (
<>
<button onClick={onToggleClick} className={ show ? 'active' : ''}>Toggle</button>
<nav className={ show ? 'show' : ''}>Navigation menu</nav>
</>
);
}

React - UseState not triggering rerender following fetch inside useEffect hook

I am just working through my first React tutorial using hooks - I am using trying to fetch local data within useEffect and then update the state using useState. I am then passing the state into a Context.provider which a child element (Card.js) subscribes to. Even though useEffect runs, the Card component and the state isn't being rerendered/updated. What am I doing wrong?
MainContext.js -
import React, { createContext, useState, useContext, useEffect, Fragment } from 'react';
import List from '../containers/List';
export const myContext= createContext();
export const MainContext = ({children}) => {
const [films, setFilms] = useState([]);
const loadData = () => {
try {
fetch('../src/assets/data.json').then( result => result.json()).then(movies => {
setFilms(films)
})
} catch (error) {
console.log('there has been an error')
}}
useEffect(() => {
loadData()
},[]);
return (
<myContext.Provider value={{films,setFilms }}>
{children()}
</myContext.Provider>
);
}
Card.js -
function Card() {
const {films} = useContext(myContext)
if (films !== undefined) {
return (
<div>
{ films.map((movie,i) => {
return (
<div key={i}>
<img src={movie.img.src} className='card-img-top' alt={movie.img.alt} />
<div className='card-body'>
<h2 className='card-title'>{`#${movie.ranking} - ${movie.title} (${movie.year})`}</h2>
</div>
<ul className='list-group list-group-flush'>
<li className='list-group-item'>{`Distributor: ${movie.distributor}`}</li>
<li className='list-group-item'>{`Amount: ${movie.amount}`}</li>
</ul></div>
)
})
}
</div>
)
} else {
return <div>{'Update Failed'}</div>
}
}
export default Card
You don't have any dependency in your useEffect array. This is why it doesn't trigger again once the app is mounted. You need to pass it a dependency, so it can run again each time the dependency value changes. Also, consider adding async before your loadData function.

Can a React portal be used in a Stateless Functional Component (SFC)?

I have used ReactDOM.createPortal inside the render method of a stateful component like so:
class MyComponent extends Component {
...
render() {
return (
<Wrapper>
{ReactDOM.createPortal(<FOO />, 'dom-location')}
</Wrapper>
)
}
}
... but can it also be used by a stateless (functional) component?
Will chime in with an option where you dont want to manually update your index.html and add extra markup, this snippet will dynamically create a div for you, then insert the children.
export const Portal = ({ children, className = 'root-portal', el = 'div' }) => {
const [container] = React.useState(() => {
// This will be executed only on the initial render
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
return document.createElement(el);
});
React.useEffect(() => {
container.classList.add(className)
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
It can be done like this for a fixed component:
const MyComponent = () => ReactDOM.createPortal(<FOO/>, 'dom-location')
or, to make the function more flexible, by passing a component prop:
const MyComponent = ({ component }) => ReactDOM.createPortal(component, 'dom-location')
can it also be used by a stateless (functional) component
?
yes.
const Modal = (props) => {
const modalRoot = document.getElementById('myEle');
return ReactDOM.createPortal(props.children, modalRoot,);
}
Inside render :
render() {
const modal = this.state.showModal ? (
<Modal>
<Hello/>
</Modal>
) : null;
return (
<div>
<div id="myEle">
</div>
</div>
);
}
Working codesandbox#demo
TSX version based on #Samuel's answer (React 17, TS 4.1):
// portal.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
interface IProps {
className? : string
el? : string
children : React.ReactNode
}
/**
* React portal based on https://stackoverflow.com/a/59154364
* #param children Child elements
* #param className CSS classname
* #param el HTML element to create. default: div
*/
const Portal : React.FC<IProps> = ( { children, className, el = 'div' } : IProps ) => {
const [container] = React.useState(document.createElement(el))
if ( className )
container.classList.add(className)
React.useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
export default Portal
IMPORTANT useRef/useState to prevent bugs
It's important that you use useState or useRef to store the element you created via document.createElement because otherwise it gets recreated on every re-render
//This div with id of "overlay-portal" needs to be added to your index.html or for next.js _document.tsx
const modalRoot = document.getElementById("overlay-portal")!;
//we use useRef here to only initialize el once and not recreate it on every rerender, which would cause bugs
const el = useRef(document.createElement("div"));
useEffect(() => {
modalRoot.appendChild(el.current);
return () => {
modalRoot.removeChild(el.current);
};
}, []);
return ReactDOM.createPortal(
<div
onClick={onOutSideClick}
ref={overlayRef}
className={classes.overlay}
>
<div ref={imageRowRef} className={classes.fullScreenImageRow}>
{renderImages()}
</div>
<button onClick={onClose} className={classes.closeButton}>
<Image width={25} height={25} src="/app/close-white.svg" />
</button>
</div>,
el.current
);
Yes, according to docs the main requirements are:
The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.
In case of stateless component you can pass element via props and render it via portal.
Hope it will helps.
Portal with SSR (NextJS)
If you are trying to use any of the above with SSR (for example NextJS) you may run into difficulty.
The following should get you what you need. This methods allows for passing in an id/selector to use for the portal which can be helpful in some cases, otherwise it creates a default using __ROOT_PORTAL__.
If it can't find the selector then it will create and attach a div.
NOTE: you could also statically add a div and specify a known id in pages/_document.tsx (or .jsx) if again using NextJS. Pass in that id and it will attempt to find and use it.
import { PropsWithChildren, useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';
export interface IPortal {
selector?: string;
}
const Portal = (props: PropsWithChildren<IPortal>) => {
props = {
selector: '__ROOT_PORTAL__',
...props
};
const { selector, children } = props;
const ref = useRef<Element>()
const [mounted, setMounted] = useState(false);
const selectorPrefixed = '#' + selector.replace(/^#/, '');
useEffect(() => {
ref.current = document.querySelector(selectorPrefixed);
if (!ref.current) {
const div = document.createElement('div');
div.setAttribute('id', selector);
document.body.appendChild(div);
ref.current = div;
}
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
};
export default Portal;
Usage
The below is a quickie example of using the portal. It does NOT take into account position etc. Just something simple to show you usage. Sky is limit from there :)
import React, { useState, CSSProperties } from 'react';
import Portal from './path/to/portal'; // Path to above
const modalStyle: CSSProperties = {
padding: '3rem',
backgroundColor: '#eee',
margin: '0 auto',
width: 400
};
const Home = () => {
const [visible, setVisible] = useState(false);
return (
<>
<p>Hello World <a href="#" onClick={() => setVisible(true)}>Show Modal</a></p>
<Portal>
{visible ? <div style={modalStyle}>Hello Modal! <a href="#" onClick={() => setVisible(false)}>Close</a></div> : null}
</Portal>
</>
);
};
export default Home;
const X = ({ children }) => ReactDOM.createPortal(children, 'dom-location')
Sharing my solution:
// PortalWrapperModal.js
import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
const PortalWrapperModal = ({
children,
onHide,
backdrop = 'static',
focus = true,
keyboard = false,
}) => {
const portalRef = useRef(null);
const handleClose = (e) => {
if (e) e.preventDefault();
if (portalRef.current) $(portalRef.current).modal('hide');
};
useEffect(() => {
if (portalRef.current) {
$(portalRef.current).modal({ backdrop, focus, keyboard });
$(portalRef.current).modal('show');
$(portalRef.current).on('hidden.bs.modal', onHide);
}
}, [onHide, backdrop, focus, keyboard]);
return ReactDOM.createPortal(
<>{children(portalRef, handleClose)}</>,
document.getElementById('modal-root')
);
};
export { PortalWrapperModal };

Resources