How to render a react template? - reactjs

I'm creating a web app that serves the user unique pages with a substantial amount of JSX e.g.
<Explanation>
<p>Here will be a lengthy section explaining how to solve a problem that I want rendered using the explanation component</p>
<Quote>It might also have child components in it</Quote>
</Explanation>
<Questions>Here will be some questions I've written</Questions>
<Image> Here will be an image that I want to render using my image component</Image>
Ideally I would like to store the JSX in a mongo database as an object like this:
post = {
_id: 1,
content: `<Explanation>
<p>Here will be a lengthy section explaining how to solve a problem that I want rendered using the explanation component</p>
<Quote>It might also have child components in it</Quote>
</Explanation>
<Questions>Here will be some questions I've written</Questions>
<Image> Here will be an image that I want to render using my image component</Image>`
because I will have many hundreds of such pages and the user should receive only a particular page.
Is there a way I can achieve this?
At the moment I have resigned to writing an object like this:
post = {
_id,
content: [
{component: Explanation,
props: {key: 'bla'},
content: [
{component: p,
props: null,
content: 'Here will be a lengthy section explaining how to solve a problem that I want rendered using the explanation component'
}
]
}
]
and then writing a function that turns them into React elements like so:
const renderPost = (post) => {
const JSX = post.content.map(c =>
React.createElement(c.component, c.props, c.content)
)
return JSX
}
but this whole process feels cumbersome and inefficient.
Is there a better way of trying to achieve my goal?

This is a quick work around.
import React from "react";
import "./styles.css";
var str = `
<Explanation>
<p>this is ex1 p tag</p>
<Quote>this is quote</Quote>
</Explanation>
<Questions>This is q</Questions>
<Image>this is img</Image>
`;
const createMarkup = (props) => ({ __html: props });
const Custom = ({ children }) => {
return <div dangerouslySetInnerHTML={createMarkup(children)} />;
};
const Quote = ({ children }) => {
return <blockquote dangerouslySetInnerHTML={createMarkup(children)} />;
};
const Questions = ({ children }) => {
return <q dangerouslySetInnerHTML={createMarkup(children)} />;
};
const Image = ({ children }) => {
return <h1 dangerouslySetInnerHTML={createMarkup(children)} />;
};
const Explanation = ({ children }) => {
return (
<div className="explain" dangerouslySetInnerHTML={createMarkup(children)} />
);
};
const cpmponentToShow = [];
const components = {
Custom,
Explanation,
Image,
Questions,
Quote
};
const componetCreate = (string) => {
const tags = string.match(/(?<=<)((\w|\b)+)[^>]/g);
if (tags.length) {
tags.map((tag) => {
var regexNew = `<${tag}>([\\s\\S]*?)</${tag}>`;
var matched = string.match(regexNew);
const leftOver = matched[1].match(/(?<=<)(\w+)[^>]/g);
if (!leftOver) {
cpmponentToShow.push(
React.createElement(
components[tag] ? components[tag] : tag,
null,
matched[1]
)
);
}
return null;
});
}
};
componetCreate(str);
export default function App() {
return (
<div className="App">
{cpmponentToShow.map((item, index) => (
<React.Fragment key={index}>{item}</React.Fragment>
))}
</div>
);
}
p {
color: red;
}
blockquote {
color: black;
}
q {
color: purple;
}
h1 {
color: royalblue;
}
.explain {
color: salmon;
}
sandbox link

Related

How to use TypeScript generics with React render props

I'm trying to type a property of a React component using generics. The problem is when I try to render the component as a render prop through another component, it seems to no longer be able to "infer" the correct type - instead it defaults to unknown and I have to use "as any" to make things work.
I've put together a working example here. Note the comments and "as any"'s: https://stackblitz.com/edit/react-ts-4oyi3d?file=index.tsx
Here's the code:
type TabProps<T> = {
data?: T;
};
type TabsProps<T> = {
children: ({
childrenArr
}: {
childrenArr: React.ReactElement<TabProps<T>>[];
}) => React.ReactElement<TabProps<T>>[];
items: React.ReactElement<TabProps<T>>[];
};
type Data = { hello: string };
const Tab = <T extends unknown>({ data }: TabProps<T>) => <div>...</div>;
const Tabs = <T extends unknown>({ children, items }: TabsProps<T>) => {
const childrenArr = useMemo(() => (Array.isArray(items) ? items : [items]), [
items
]);
// ...
// In real application this is where manipulation of the elements in the childrenArr would take place
// Since this is just an example I'll just do this
const manipulatedchildrenArr = childrenArr.map(child => {
return {
...(child as any),
props: { data: { hello: "world is manipulated" } }
};
});
return (
<div>
<div>Hello</div>
<div>
{typeof children === "function"
? children({ childrenArr: manipulatedchildrenArr })
: children}
</div>
</div>
);
};
const App = () => {
const data: Data = { hello: "World" };
return (
<div className="App">
<Tabs items={[<Tab data={data} />]}>
{({ childrenArr }) =>
childrenArr.map(child => (
// Remove "as any" and it will be type unknown and result in an error
<div>{(child.props as any).data.hello}</div>
))
}
</Tabs>
</div>
);
};
As you can see the type of the data prop is lost.
Now I'm not sure if I went outside the scope of what you were looking for and If I did please let me know and I'll adjust the solution..
Update: I forgot to add code for Single tab.
import React from "react";
import ReactDOM from "react-dom";
export interface ITabProps<T> {
data?: T;
handleProcessData: (data: T) => string;
}
export function Tab<T>(props: ITabProps<T>) {
const data = props.data ? props.handleProcessData(props.data) : "None";
return <div>Hello {data}</div>;
}
export type TabElement<T> = React.ReactElement<ITabProps<T>> | React.ReactElement<ITabProps<T>>[]
export interface ITabsProps<T> {
handleManipulation: (data: T) => T;
children: TabElement<T>
}
export function Tabs<T>(props: ITabsProps<T>) {
const array = [] as TabElement<T>[];
if (Array.isArray(props.children))
props.children.forEach((child) => {
let mChild = <Tab<T> handleProcessData={child.props.handleProcessData} data={props.handleManipulation(child.props.data)} /> as TabElement<T>;
array.push(mChild)
})
else {
let mChild = <Tab<T> handleProcessData={props.children.props.handleProcessData} data={props.handleManipulation(props.children.props.data)} /> as TabElement<T>;
array.push(mChild)
}
return <div>{array.map((item) => (item))}</div>;
}
export type Data = { hello: string };
export function App() {
//B.C. it's generic you going to have to have some form of generic control functions
const handleProcessData = (data: Data) => {
//Here you have to specifiy how this specific data type is processed
return data.hello;
};
const handleManipulation = (data: Data) => {
//here you would have all your manipulation logic
return { hello: data.hello + " is manipulated" };
}
//To Make this easier to use you could nest handleProcessData inside the Tabs component
return (
<div>
<Tabs<Data> handleManipulation={handleManipulation}>
<Tab<Data> handleProcessData={handleProcessData} data={{hello: "world1"}} />
<Tab<Data> handleProcessData={handleProcessData} data={{hello: "world2"}} />
</Tabs>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));

How to use React Ref with Styled Component

I am running React 16.8.6 and Styled Components 4.3.2 currently and am hitting an issue trying to use React.forwardRef.
In Comments.js
import CommentItem from './CommentItem';
class Comments extends Component {
constructor(props) {
super(props);
this.items = [];
}
componentDidMount() {
console.log(this.items);
}
render() {
const { comments } = this.props;
return (
<div>
{comments.length > 0 &&
comments.map((comment, i) => {
this.items[i] = React.createRef();
return (
<CommentItem
ref={this.items[i]}
key={comment.id}
comment={comment}
/>
);
})}
</div>
);
}
}
In CommentItem.js
import styled from "styled-components";
import { Media } from "react-bulma-components";
const CommentMediaWrapper = styled(Media)`
position: relative;
`;
const CommentItem = React.forwardRef(
({ comment }, ref) => {
return (
<CommentMediaWrapper ref={ref}>
<div>...</div>
</CommentMediaWrapper>
);
}
);
export default CommentItem;
In Comments.js will console an array [{ current: null },...]
I don't know how to pass ref in to CommentMediaWrapper. I don't want add new element outer or inner CommentMediaWrapper. Thank for your help.
I think the issue here is more to do with the library element Media from react-bulma-components that you are wrapping. This component exposes a prop named domRef that can be used to pass the DOM reference to the HTML element underneath the wrappers.
const CommentItem = React.forwardRef(
({ comment }, ref) => {
return (
<CommentMediaWrapper domRef={ref}>
<div>...</div>
</CommentMediaWrapper>
);
}
);
Github Reference

React-Select with React-Apollo does not work

We are using react-select and fetching the items as the user types. I am not able to make it work with react-apollo.
Can someone help me provide a guideline?
Here is my unsuccessful attempt:
class PatientSearchByPhone extends Component {
updateProp = mobile => {
if (mobile.length < 10) return;
this.props.data.refetch({ input: { mobile } });
};
render() {
console.log(this.props.data);
return <AsyncSelect cacheOptions loadOptions={this.updateProp} />;
}
}
const FETCH_PATIENT = gql`
query Patient($input: PatientSearchInput) {
getPatients(input: $input) {
id
first_name
}
}
`;
export default graphql(FETCH_PATIENT, {
options: ({ mobile }) => ({ variables: { input: { mobile } } })
})(PatientSearchByPhone);
Versions:
"react-apollo": "^2.1.11",
"react-select": "^2.1.0"
Thanks for your time.
I got an e-mail asking a response to this question. It reminds me of this XKCD comics:
I do not recall the exact solution I implemented, so I setup a complete example for this.
This app (code snippet below) kickstarts searching as soon as you type 4 characters or more in the input box (You are expected to type artist's name. Try vinci?). Here is the code:
import React, { useState } from "react";
import "./App.css";
import AsyncSelect from "react-select/async";
import ApolloClient, { gql } from "apollo-boost";
const client = new ApolloClient({
uri: "https://metaphysics-production.artsy.net"
});
const fetchArtists = async (input: string, cb: any) => {
if (input && input.trim().length < 4) {
return [];
}
const res = await client.query({
query: gql`
query {
match_artist(term: "${input}") {
name
imageUrl
}
}
`
});
if (res.data && res.data.match_artist) {
return res.data.match_artist.map(
(a: { name: string; imageUrl: string }) => ({
label: a.name,
value: a.imageUrl
})
);
}
return [];
};
const App: React.FC = () => {
const [artist, setArtist] = useState({
label: "No Name",
value: "https://dummyimage.com/200x200/000/fff&text=No+Artist"
});
return (
<div className="App">
<header className="App-header">
<h4>Search artists and their image (type 4 char or more)</h4>
<AsyncSelect
loadOptions={fetchArtists}
onChange={(opt: any) => setArtist(opt)}
placeholder="Search an Artist"
className="select"
/>
<div>
<img alt={artist.label} src={artist.value} className="aimage" />
</div>
</header>
</div>
);
};
export default App;
You can clone https://github.com/naishe/react-select-apollo it is a working example. I have deployed the app here: https://apollo-select.naishe.in/, may be play a little?
The other option is to execute the graphql query manually using the client that is exposed by wrapping the base component with withApollo.
In the example below, we have,
BaseComponnent which renders the AsyncSelect react-select component
loadOptionsIndexes which executes the async graphql fetch via the client
BaseComponent.propTypes describes the required client prop
withApollo wraps the base component to give us the actual component we'll use elsewhere in the react app.
const BaseComponent = (props) => {
const loadOptionsIndexes = (inputValue) => {
let graphqlQueryExpression = {
query: QUERY_INDEXES,
variables: {
name: inputValue
}
}
const transformDataIntoValueLabel = (data) => {
return data.indexes.indexes.map(ix => { return { value: ix.id, label: ix.name }})
}
return new Promise(resolve => {
props.client.query(graphqlQueryExpression).then(response => {
resolve(transformDataIntoValueLabel(response.data))
})
});
}
return (
<>
<div className="chart-buttons-default">
<div className="select-index-input" style={{width: 400, display: "inline-block"}}>
<AsyncSelect
isMulti={true}
cacheOptions={true}
defaultOptions={true}
loadOptions={loadOptionsIndexes} />
</div>
</div>
</>
)
}
BaseComponent.propTypes = {
client: PropTypes.any,
}
const ComplementComponent = withApollo(BaseComponent);
Sorry if the example is a little off - copy and pasted what I had working rather than moving on without giving back.

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 };

How to scroll to bottom in react?

I want to build a chat system and automatically scroll to the bottom when entering the window and when new messages come in. How do you automatically scroll to the bottom of a container in React?
As Tushar mentioned, you can keep a dummy div at the bottom of your chat:
render () {
return (
<div>
<div className="MessageContainer" >
<div className="MessagesList">
{this.renderMessages()}
</div>
<div style={{ float:"left", clear: "both" }}
ref={(el) => { this.messagesEnd = el; }}>
</div>
</div>
</div>
);
}
and then scroll to it whenever your component is updated (i.e. state updated as new messages are added):
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
I'm using the standard Element.scrollIntoView method here.
I just want to update the answer to match the new React.createRef() method, but it's basically the same, just have in mind the current property in the created ref:
class Messages extends React.Component {
const messagesEndRef = React.createRef()
componentDidMount () {
this.scrollToBottom()
}
componentDidUpdate () {
this.scrollToBottom()
}
scrollToBottom = () => {
this.messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
render () {
const { messages } = this.props
return (
<div>
{messages.map(message => <Message key={message.id} {...message} />)}
<div ref={this.messagesEndRef} />
</div>
)
}
}
UPDATE:
Now that hooks are available, I'm updating the answer to add the use of the useRef and useEffect hooks, the real thing doing the magic (React refs and scrollIntoView DOM method) remains the same:
import React, { useEffect, useRef } from 'react'
const Messages = ({ messages }) => {
const messagesEndRef = useRef(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [messages]);
return (
<div>
{messages.map(message => <Message key={message.id} {...message} />)}
<div ref={messagesEndRef} />
</div>
)
}
Also made a (very basic) codesandbox if you wanna check the behaviour https://codesandbox.io/s/scrolltobottomexample-f90lz
Do not use findDOMNode
Class components with ref
class MyComponent extends Component {
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
scrollToBottom() {
this.el.scrollIntoView({ behavior: 'smooth' });
}
render() {
return <div ref={el => { this.el = el; }} />
}
}
Function components with hooks:
import React, { useRef, useEffect } from 'react';
const MyComponent = () => {
const divRef = useRef(null);
useEffect(() => {
divRef.current.scrollIntoView({ behavior: 'smooth' });
});
return <div ref={divRef} />;
}
Thanks to #enlitement
we should avoid using findDOMNode,
we can use refs to keep track of the components
render() {
...
return (
<div>
<div
className="MessageList"
ref={(div) => {
this.messageList = div;
}}
>
{ messageListContent }
</div>
</div>
);
}
scrollToBottom() {
const scrollHeight = this.messageList.scrollHeight;
const height = this.messageList.clientHeight;
const maxScrollTop = scrollHeight - height;
this.messageList.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
}
componentDidUpdate() {
this.scrollToBottom();
}
reference:
https://facebook.github.io/react/docs/react-dom.html#finddomnode
https://www.pubnub.com/blog/2016-06-28-reactjs-chat-app-infinite-scroll-history-using-redux/
The easiest and best way I would recommend is.
My ReactJS version: 16.12.0
For Class Components
HTML structure inside render() function
render()
return(
<body>
<div ref="messageList">
<div>Message 1</div>
<div>Message 2</div>
<div>Message 3</div>
</div>
</body>
)
)
scrollToBottom() function which will get reference of the element.
and scroll according to scrollIntoView() function.
scrollToBottom = () => {
const { messageList } = this.refs;
messageList.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
}
and call the above function inside componentDidMount() and componentDidUpdate()
For Functional Components (Hooks)
Import useRef() and useEffect()
import { useEffect, useRef } from 'react'
Inside your export function, (same as calling a useState())
const messageRef = useRef();
And let's assume you have to scroll when page load,
useEffect(() => {
if (messageRef.current) {
messageRef.current.scrollIntoView(
{
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}
})
OR if you want it to trigger once an action performed,
useEffect(() => {
if (messageRef.current) {
messageRef.current.scrollIntoView(
{
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}
},
[stateVariable])
And Finally, to your HTML structure
return(
<body>
<div ref={messageRef}> // <= The only different is we are calling a variable here
<div>Message 1</div>
<div>Message 2</div>
<div>Message 3</div>
</div>
</body>
)
for more explanation about Element.scrollIntoView() visit developer.mozilla.org
More detailed explanation in Callback refs visit reactjs.org
react-scrollable-feed automatically scrolls down to the latest element if the user was already at the bottom of the scrollable section. Otherwise, it will leave the user at the same position. I think this is pretty useful for chat components :)
I think the other answers here will force scroll everytime no matter where the scrollbar was. The other issue with scrollIntoView is that it will scroll the whole page if your scrollable div was not in view.
It can be used like this :
import * as React from 'react'
import ScrollableFeed from 'react-scrollable-feed'
class App extends React.Component {
render() {
const messages = ['Item 1', 'Item 2'];
return (
<ScrollableFeed>
{messages.map((message, i) => <div key={i}>{message}</div>)}
</ScrollableFeed>
);
}
}
Just make sure to have a wrapper component with a specific height or max-height
Disclaimer: I am the owner of the package
I could not get any of below answers to work but simple js did the trick for me:
window.scrollTo({
top: document.body.scrollHeight,
left: 0,
behavior: 'smooth'
});
If you want to do this with React Hooks, this method can be followed. For a dummy div has been placed at the bottom of the chat. useRef Hook is used here.
Hooks API Reference : https://reactjs.org/docs/hooks-reference.html#useref
import React, { useEffect, useRef } from 'react';
const ChatView = ({ ...props }) => {
const el = useRef(null);
useEffect(() => {
el.current.scrollIntoView({ block: 'end', behavior: 'smooth' });
});
return (
<div>
<div className="MessageContainer" >
<div className="MessagesList">
{this.renderMessages()}
</div>
<div id={'el'} ref={el}>
</div>
</div>
</div>
);
}
There are two major problems with the scrollIntoView(...) approach in the top answers:
it's semantically incorrect, as it causes the entire page to scroll if your parent element is scrolled outside the window boundaries. The browser literally scrolls anything it needs to in getting the element visible.
in a functional component using useEffect(), you get unreliable results, at least in Chrome 96.0.4665.45. useEffect() gets called too soon on page reload and the scroll doesn't happen. Delaying scrollIntoView with setTimeout(..., 0) fixes it for page reload, but not first load in a fresh tab, at least for me. shrugs
Here's the solution I've been using, it's solid and is more compatible with older browsers:
function Chat() {
const chatParent = useRef<HTMLDivElement(null);
useEffect(() => {
const domNode = chatParent.current;
if (domNode) {
domNode.scrollTop = domNode.scrollHeight;
}
})
return (
<div ref={chatParent}>
...
</div>
)
}
You can use refs to keep track of the components.
If you know of a way to set the ref of one individual component (the last one), please post!
Here's what I found worked for me:
class ChatContainer extends React.Component {
render() {
const {
messages
} = this.props;
var messageBubbles = messages.map((message, idx) => (
<MessageBubble
key={message.id}
message={message.body}
ref={(ref) => this['_div' + idx] = ref}
/>
));
return (
<div>
{messageBubbles}
</div>
);
}
componentDidMount() {
this.handleResize();
// Scroll to the bottom on initialization
var len = this.props.messages.length - 1;
const node = ReactDOM.findDOMNode(this['_div' + len]);
if (node) {
node.scrollIntoView();
}
}
componentDidUpdate() {
// Scroll as new elements come along
var len = this.props.messages.length - 1;
const node = ReactDOM.findDOMNode(this['_div' + len]);
if (node) {
node.scrollIntoView();
}
}
}
Reference your messages container.
<div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
Find your messages container and make its scrollTop attribute equal scrollHeight:
scrollToBottom = () => {
const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
Evoke above method on componentDidMount and componentDidUpdate.
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
This is how I am using this in my code:
export default class StoryView extends Component {
constructor(props) {
super(props);
this.scrollToBottom = this.scrollToBottom.bind(this);
}
scrollToBottom = () => {
const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
render() {
return (
<div>
<Grid className="storyView">
<Row>
<div className="codeView">
<Col md={8} mdOffset={2}>
<div ref={(el) => { this.messagesContainer = el; }}
className="chat">
{
this.props.messages.map(function (message, i) {
return (
<div key={i}>
<div className="bubble" >
{message.body}
</div>
</div>
);
}, this)
}
</div>
</Col>
</div>
</Row>
</Grid>
</div>
);
}
}
I created a empty element in the end of messages, and scrolled to that element. No need of keeping track of refs.
Working Example:
You can use the DOM scrollIntoView method to make a component visible in the view.
For this, while rendering the component just give a reference ID for the DOM element using ref attribute. Then use the method scrollIntoView on componentDidMount life cycle. I am just putting a working sample code for this solution. The following is a component rendering each time a message received. You should write code/methods for rendering this component.
class ChatMessage extends Component {
scrollToBottom = (ref) => {
this.refs[ref].scrollIntoView({ behavior: "smooth" });
}
componentDidMount() {
this.scrollToBottom(this.props.message.MessageId);
}
render() {
return(
<div ref={this.props.message.MessageId}>
<div>Message content here...</div>
</div>
);
}
}
Here this.props.message.MessageId is the unique ID of the particular chat message passed as props
import React, {Component} from 'react';
export default class ChatOutPut extends Component {
constructor(props) {
super(props);
this.state = {
messages: props.chatmessages
};
}
componentDidUpdate = (previousProps, previousState) => {
if (this.refs.chatoutput != null) {
this.refs.chatoutput.scrollTop = this.refs.chatoutput.scrollHeight;
}
}
renderMessage(data) {
return (
<div key={data.key}>
{data.message}
</div>
);
}
render() {
return (
<div ref='chatoutput' className={classes.chatoutputcontainer}>
{this.state.messages.map(this.renderMessage, this)}
</div>
);
}
}
thank you 'metakermit' for his good answer, but I think we can make it a bit better,
for scroll to bottom, we should use this:
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
}
but if you want to scroll top, you should use this:
scrollToTop = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
}
and this codes are common:
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
render () {
return (
<div>
<div className="MessageContainer" >
<div className="MessagesList">
{this.renderMessages()}
</div>
<div style={{ float:"left", clear: "both" }}
ref={(el) => { this.messagesEnd = el; }}>
</div>
</div>
</div>
);
}
As another option it is worth looking at react scroll component.
I like doing it the following way.
componentDidUpdate(prevProps, prevState){
this.scrollToBottom();
}
scrollToBottom() {
const {thing} = this.refs;
thing.scrollTop = thing.scrollHeight - thing.clientHeight;
}
render(){
return(
<div ref={`thing`}>
<ManyThings things={}>
</div>
)
}
This is how you would solve this in TypeScript (using the ref to a targeted element where you scroll to):
class Chat extends Component <TextChatPropsType, TextChatStateType> {
private scrollTarget = React.createRef<HTMLDivElement>();
componentDidMount() {
this.scrollToBottom();//scroll to bottom on mount
}
componentDidUpdate() {
this.scrollToBottom();//scroll to bottom when new message was added
}
scrollToBottom = () => {
const node: HTMLDivElement | null = this.scrollTarget.current; //get the element via ref
if (node) { //current ref can be null, so we have to check
node.scrollIntoView({behavior: 'smooth'}); //scroll to the targeted element
}
};
render <div>
{message.map((m: Message) => <ChatMessage key={`chat--${m.id}`} message={m}/>}
<div ref={this.scrollTarget} data-explanation="This is where we scroll to"></div>
</div>
}
For more information about using ref with React and Typescript you can find a great article here.
This works for me
messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight
where const messagesEndRef = useRef(); to use
Using React.createRef()
class MessageBox extends Component {
constructor(props) {
super(props)
this.boxRef = React.createRef()
}
scrollToBottom = () => {
this.boxRef.current.scrollTop = this.boxRef.current.scrollHeight
}
componentDidUpdate = () => {
this.scrollToBottom()
}
render() {
return (
<div ref={this.boxRef}></div>
)
}
}
This is modified from an answer above to support 'children' instead of a data array.
Note: The use of styled-components is of no importance to the solution.
import {useEffect, useRef} from "react";
import React from "react";
import styled from "styled-components";
export interface Props {
children: Array<any> | any,
}
export function AutoScrollList(props: Props) {
const bottomRef: any = useRef();
const scrollToBottom = () => {
bottomRef.current.scrollIntoView({
behavior: "smooth",
block: "start",
});
};
useEffect(() => {
scrollToBottom()
}, [props.children])
return (
<Container {...props}>
<div key={'child'}>{props.children}</div>
<div key={'dummy'} ref={bottomRef}/>
</Container>
);
}
const Container = styled.div``;
In order to scroll down to the bottom of the page first we have to select an id which resides at the bottom of the page. Then we can use the document.getElementById to select the id and scroll down using scrollIntoView(). Please refer the below code.
scrollToBottom= async ()=>{
document.getElementById('bottomID').scrollIntoView();
}
I have face this problem in mweb/web.All the solution is good in this page but all the solution is not working while using android chrome browser .
So for mweb and web I got the solution with some minor fixes.
import { createRef, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'redux/store';
import Message from '../Message/Message';
import styles from './MessageList.module.scss';
const MessageList = () => {
const messagesEndRef: any = createRef();
const { messages } = useSelector((state: AppState) => state?.video);
const scrollToBottom = () => {
//this is not working in mWeb
// messagesEndRef.current.scrollIntoView({
// behavior: 'smooth',
// block: 'end',
// inline: 'nearest',
// });
const scroll =
messagesEndRef.current.scrollHeight -
messagesEndRef.current.clientHeight;
messagesEndRef.current.scrollTo(0, scroll);
};
useEffect(() => {
if (messages.length > 3) {
scrollToBottom();
}
}, [messages]);
return (
<section className={styles.footerTopSection} ref={messagesEndRef} >
{messages?.map((message: any) => (
<Message key={message.id} {...message} />
))}
</section>
);
};
export default MessageList;
This is a great usecase for useLayoutEffect as taught by Kent C. Dodds.
https://kentcdodds.com/blog/useeffect-vs-uselayouteffect
if your effect is mutating the DOM (via a DOM node ref) and the DOM mutation will change the appearance of the DOM node between the time that it is rendered and your effect mutates it, then you don't want to use useEffect.
In my case i was dynamically generating elements at the bottom of a div so i had to add a small timeout.
const bottomRef = useRef<null | HTMLDivElement>(null);
useLayoutEffect(() => {
setTimeout(function () {
if (bottomRef.current) bottomRef.current.scrollTop = bottomRef.current.scrollHeight;
}, 10);
}, [transactionsAmount]);
const scrollingBottom = () => {
const e = ref;
e.current?.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "start",
});
};
useEffect(() => {
scrollingBottom();
});
<span ref={ref}>{item.body.content}</span>
Full version (Typescript):
import * as React from 'react'
export class DivWithScrollHere extends React.Component<any, any> {
loading:any = React.createRef();
componentDidMount() {
this.loading.scrollIntoView(false);
}
render() {
return (
<div ref={e => { this.loading = e; }}> <LoadingTile /> </div>
)
}
}

Resources