简体   繁体   中英

ReactJS and Twilio - TypeError: this.props.history not defined

Learning React for the first time, and opted to use Twilio's chat app demo to get a jump start.

I can render the welcome screen without issue, but not routed to the chat screen/room when I login. Including link to demo, code snippets, notes and more below.

Anyone out there see what's going on here and can advise? I've fixed a few issues so far that came up due to updates since the demo was posted (change Switch to Routes , etc.), but haven't been able to get past this TypeError . Any and all help is welcome and TIA!

Link to Twilio Demo: Twilio Programmable Chat App Demo

The error is raised in the login() function, at the line: this.props.history.push('chat', { email, room }); and the error reads Uncaught TypeError: this.props.history is undefined .

As an aside, I have attempted to import the withRouter method from react-router-dom but the method is not exported from react-router-dom and all information I am finding online about this method points to an older version of react-router-dom than what I am working with, so this is not a workable solution. I've also tried to apply .bind(this) on the onClick that calls login() , but this did not work either.

WelcomeScreen.js

import React from "react";
import {
  Grid,
  TextField,
  Card,
  AppBar,
  Toolbar,
  Typography,
  Button,
 } from "@material-ui/core";


class WelcomeScreen extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
        email: "",
        room: "",
    };
}

login = () => {
    const { room, email } = this.state;
    if (room && email) {
        this.props.history.push("chat", { room, email });
    }
}

handleChange = (event) => {
    this.setState({ [event.target.name]: event.target.value });
};

render() {
    const { email, room } = this.state;
    return (
      <>
        <AppBar style={styles.header} elevation={10}>
          <Toolbar>
            <Typography variant="h6">
              Chat App with Twilio Programmable Chat and React
            </Typography>
          </Toolbar>
        </AppBar>
        <Grid
          style={styles.grid}
          container
          direction="column"
          justify="center"
          alignItems="center">
          <Card style={styles.card} elevation={10}>
            <Grid item style={styles.gridItem}>
              <TextField
                name="email"
                required
                style={styles.textField}
                label="Email address"
                placeholder="Enter email address"
                variant="outlined"
                type="email"
                value={email}
                onChange={this.handleChange}/>
            </Grid>
            <Grid item style={styles.gridItem}>
              <TextField
                name="room"
                required
                style={styles.textField}
                label="Room"
                placeholder="Enter room name"
                variant="outlined"
                value={room}
                onChange={this.handleChange}/>
            </Grid>
            <Grid item style={styles.gridItem}>
              <Button
                color="primary"
                variant="contained"
                style={styles.button}
                onClick={this.login}>
                Login
              </Button>
            </Grid>
          </Card>
        </Grid>
      </>
    );
  }      
}

const styles = {
  header: {},
  grid: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
  card: { padding: 40 },
  textField: { width: 300 },
  gridItem: { paddingTop: 12, paddingBottom: 12 },
  button: { width: 300 },
};  

export default WelcomeScreen;  

ChatScreen.js

import React from "react";
import {
  AppBar,
  Backdrop,
  CircularProgress,
  Container,
  CssBaseline,
  Grid,
  IconButton,
  List,
  TextField,
  Toolbar,
  Typography,
} from "@material-ui/core";
import { Send } from "@material-ui/icons";
import axios from "axios";
import ChatItem from "./ChatItem";
const Chat = require("twilio-chat");


 class ChatScreen extends React.Component {
   constructor(props) {
    super(props);

    this.state = {
        text: "",
        messages: [],
        loading: false,
        channel: null,
    };

    this.scrollDiv = React.createRef();
}

componentDidMount = async () => {
    const { location } = this.props;
    const { state } = location || {};
    const { email, room } = state || {};
    let token = "";
  
    if (!email || !room) {
        this.props.history.replace("/");
    }
  
    this.setState({ loading: true });
  
    try {
        token = await this.getToken(email);
    } catch {
        throw new Error("Unable to get token, please reload this page");
    }

    const client = await Chat.Client.create(token);

    client.on("tokenAboutToExpire", async () => {
        const token = await this.getToken(email);
        client.updateToken(token);
    });

    client.on("tokenExpired", async () => {
        const token = await this.getToken(email);
        client.updateToken(token);
    });

    client.on("channelJoined", async (channel) => {
        // getting list of all messages since this is an existing channel
        const messages = await channel.getMessages();
        this.setState({ messages: messages.items || [] });
        this.scrollToBottom();
    });
    
    try {
        const channel = await client.getChannelByUniqueName(room);
        this.joinChannel(channel);
    } catch(err) {
        try {
            const channel = await client.createChannel({
                uniqueName: room,
                friendlyName: room,
        });
      
            this.joinChannel(channel);
        } catch {
            throw new Error("Unable to create channel, please reload this page");
        }
    }
}
  
getToken = async (email) => {
    const response = await axios.get(`http://localhost:5000/token/${email}`);
    const { data } = response;
    return data.token;
}

joinChannel = async (channel) => {
    if (channel.channelState.status !== "joined") {
        await channel.join();
   }
 
    this.setState({ 
        channel:channel, 
        loading: false 
    });
 
   channel.on("messageAdded", this.handleMessageAdded);
   this.scrollToBottom();
};
 
 
handleMessageAdded = (message) => {
    const { messages } = this.state;
    this.setState({
        messages: [...messages, message],
    },
        this.scrollToBottom
    );
};
 
scrollToBottom = () => {
    const scrollHeight = this.scrollDiv.current.scrollHeight;
    const height = this.scrollDiv.current.clientHeight;
    const maxScrollTop = scrollHeight - height;
    this.scrollDiv.current.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
};

sendMessage = () => {
    const { text, channel } = this.state;
    if (text) {
        this.setState({ loading: true });
        channel.sendMessage(String(text).trim());
        this.setState({ text: "", loading: false });
    }
};

render() {
    const { loading, text, messages, channel } = this.state;
    const { location } = this.props;
    const { state } = location || {};
    const { email, room } = state || {};
  
    return (
      <Container component="main" maxWidth="md">
        <Backdrop open={loading} style={{ zIndex: 99999 }}>
          <CircularProgress style={{ color: "white" }} />
        </Backdrop>
  
        <AppBar elevation={10}>
          <Toolbar>
            <Typography variant="h6">
              {`Room: ${room}, User: ${email}`}
            </Typography>
          </Toolbar>
        </AppBar>
  
        <CssBaseline />
  
        <Grid container direction="column" style={styles.mainGrid}>
          <Grid item style={styles.gridItemChatList} ref={this.scrollDiv}>
            <List dense={true}>
                {messages &&
                  messages.map((message) => 
                    <ChatItem
                      key={message.index}
                      message={message}
                      email={email}/>
                  )}
            </List>
          </Grid>
  
          <Grid item style={styles.gridItemMessage}>
            <Grid
              container
              direction="row"
              justify="center"
              alignItems="center">
              <Grid item style={styles.textFieldContainer}>
                <TextField
                  required
                  style={styles.textField}
                  placeholder="Enter message"
                  variant="outlined"
                  multiline
                  rows={2}
                  value={text}
                  disabled={!channel}
                  onChange={(event) =>
                    this.setState({ text: event.target.value })
                  }/>
              </Grid>
              
              <Grid item>
                <IconButton
                  style={styles.sendButton}
                  onClick={this.sendMessage}
                  disabled={!channel}>
                  <Send style={styles.sendIcon} />
                </IconButton>
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      </Container>
    );
  }      
}

const styles = {
    textField: { width: "100%", borderWidth: 0, borderColor: "transparent" },
    textFieldContainer: { flex: 1, marginRight: 12 },
    gridItem: { paddingTop: 12, paddingBottom: 12 },
    gridItemChatList: { overflow: "auto", height: "70vh" },
    gridItemMessage: { marginTop: 12, marginBottom: 12 },
    sendButton: { backgroundColor: "#3f51b5" },
    sendIcon: { color: "white" },
    mainGrid: { paddingTop: 100, borderWidth: 1 },
};

export default ChatScreen;

Router.js

import React from "react";
import { BrowserRouter, Routes, Route, } from "react-router-dom";
import WelcomeScreen from "./WelcomeScreen";
import ChatScreen from "./ChatScreen";

function Router() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/chat" element={<ChatScreen/>} />
        <Route path="/" element={<WelcomeScreen/>} />
      </Routes>
    </BrowserRouter>
  );
}

export default Router;

If ChatScreen and WindowScreen are functional Components , then you can directly use useNavigate instead of useHistory . But can't find anything specific for class component. It is given in migration from v5 section of the documentation.

https://reactrouter.com/docs/en/v6/upgrading/v5

You can search for useNavigate .


Solution ( workaround )

You can create a Function Wrapper Component around your class components and in that component, you can pass props like

  • navigate from useNavigate

I have taken an idea from HOC components in react. Here is an example of what I tried and have worked. https://codesandbox.io/s/snowy-moon-30br5?file=/src/Comp.js

This can work till we don't find a legitimate method of doing the same.

you can click on Click button in route /1 and it successfully navigates to route /2

Note: See the console for the passed props in the Sandox's console

Twilio developer evangelist here.

In your Router.js file you have:

        <Route path="/chat" element={<ChatScreen/>} />
        <Route path="/" element={<WelcomeScreen/>} />

The correct attribute is component not element . So your Router.js should look like this:

        <Route path="/chat" component={<ChatScreen/>} />
        <Route path="/"     component={<WelcomeScreen/>} />

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM