I'm trying to implement socket.io library to a React application for practice. It's my first time I implement redux in any kind for application.
Describe the problem
I created a component Chatroom.js where in componentDidMount I dispatch an action to connect to the socket and listen for events.
componentDidMount() {
console.log('---Chatroom did mount')
console.log('isLoaded: ' + this.props.socket.isLoaded)
// if I remove this if-statement the compoenent re-renders
// and a new socket is created
if (!this.props.socket.isLoaded) {
this.props.userLoginToSocket()
.then(() => this.props.receive())
.then(() => this.props.setLoaded(true))
.then(() => this.props.sendUsername(this.props.auth.user.username))
}
}
I implemented a redux middleware to handle the socket.io communication as proposed in this post.
When I start the application I get this log
Chatroom.js:55 ---Chatroom did mount
Chatroom.js:58 isLoaded: false
Chatroom.js:76 ---Chatroom will unmount
Messages.js:38 Messages component didMount
Chatroom.js:55 ---Chatroom did mount
Chatroom.js:58 isLoaded: true
And the componentWillReceiveProps never gets executed.
I don't think that this is the expected behaviour and the componentDidMount should be only called once. Furthermore I cannot understand why the componentWillUnmount gets fired.
When I recieve a message from the server the log is
Chatroom.js:76 ---Chatroom will unmount
Messages.js:38 Messages component didMount
Chatroom.js:55 ---Chatroom did mount
Chatroom.js:58 isLoaded: true
which clearly indicates that every time I dispatch an action the component unmounts and remounts.
Full Code
You can find the full project's code at github.
// ./Chatroom.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { userLoginToSocket, receive, sendUsername, disconnect, setLoaded, emit } from '../actions/socketAction';
import Messages from './Messages'
class Chatroom extends Component {
constructor(props) {
super(props)
this.handleSend = this.handleSend.bind(this)
}
componentDidMount() {
console.log('---Chatroom did mount')
// console.log('socket.isConnected: ' + this.props.socket.isConnected)
// console.log('socket.isConnecting: ' + this.props.socket.isConnecting)
console.log('isLoaded: ' + this.props.socket.isLoaded)
// if I remove this if-statement the compoenent re-renders
// and a new socket is created
if (!this.props.socket.isLoaded) {
this.props.userLoginToSocket()
.then(() => this.props.receive())
.then(() => this.props.setLoaded(true))
.then(() => this.props.sendUsername(this.props.auth.user.username))
}
}
componentWillUnmount() {
// every time a new message is recieved the component willUnmount
// i want on component will unmount to disconnect from the socket
console.log('---Chatroom will unmount')
}
componentWillReceiveProps(nextProps) {
console.log('---Component will receive props')
}
handleSend() {
this.props.emit('Hello')
}
render() {
return (
<div>
<h1>Chatroom</h1>
{ this.props.socket.isLoaded &&
<Messages messages={this.props.messages.messages}/>
}
<button onClick={this.handleSend}>Send</button>
</div>
);
}
}
Chatroom.propTypes = {
socket: PropTypes.object,
messages: PropTypes.object
}
const mapStateToProps = (state) => {
return({
socket: state.socket,
messages: state.messages
})
}
export default withRouter(
connect(mapStateToProps, { userLoginToSocket , receive, sendUsername, disconnect, setLoaded, emit })(Chatroom)
)
// ./Messages.js
import React, { Component } from 'react'
class Messages extends Component {
componentDidMount() {
console.log('Messages component didMount')
}
render() {
return (
<div>
<h3>Messages</h3>
<ul>
{this.props.messages.map((item, ind) => <li key={ind}>{item.message}</li>)}
</ul>
</div>
)
}
}
export default Messages
Actions
// ../actions/socketAction
export const setLoaded = (boolean) => {
return {
type : "SET_LOADED",
boolean
}
}
export const userLoginToSocket = () => {
return (dispatch) => {
return dispatch({
type: 'socket',
types: ["CONNECT", "CONNECT_SUCCESS", "CONNECT_FAIL"],
promise: (socket) => socket.connect()
});
}
}
export function disconnect() {
return {
type: 'socket',
types: ["DISCONNECT", "DISCONNECT_SUCCESS", "DISCONNECT_FAIL"],
promise: socket => socket.disconnect(),
}
}
export const receive = () => {
return (dispatch) => {
const newMessage = (message) => {
return dispatch({
type: "NEW_MESSAGE_FROM_SOCKET",
payload: message,
});
};
return dispatch({
type: 'socket',
types: ["RECEIVE_", "RECEIVE_SUCC", "RECEIVE_FAIL"],
promise: (socket) => socket.on('ReceiveMessage', newMessage),
});
}
}
export const sendUsername = (user) => {
return (dispatch) => {
return dispatch({
type: 'socket',
types: ["SEND_USER", "SEND_USER_SUCCESS", "SEND_USER_FAIL"],
promise: (socket) => socket.emit('SET_USERNAME', user),
});
}
}
export const emit = (message) => {
return (dispatch) => {
return dispatch({
type: 'socket',
types: ["SEND", "SEND_SUCCESS", "SEND_FAIL"],
promise: (socket) => socket.emit('SEND_MESSAGE', message),
});
}
}
Reducers
// ../socketReducer.js
const initialState = {
isConnected: false,
isConnecting: false,
isLoaded: false,
messageRecieved: false
}
export default function socketReducer(state=initialState, action) {
switch (action.type) {
case "CONNECTED":
return {
...state,
isConnected: true
}
case "DISCONNECTED":
return {
...state,
isConnected: false
}
case "NEW_MESSAGE_FROM_SOCKET":
return {
...state,
messageRecieved: true
}
case "SET_LOADED":
return {
...state,
isLoaded: action.boolean
}
default:
return state
}
}
// ../messagesReducer.js
const initialState = {
messages: [{message: "initial"}],
isRecieving: false,
didRecieve: false
}
export default function(state = initialState, action) {
switch (action.type) {
case "NEW_MESSAGE_FROM_SOCKET":
return {
...state,
messages: [...state.messages, action.payload]
}
default :
return state
}
}
Related
For the web app I'm building in React, I need to record audio and be able to somehow put that recorded audio in the app's global state so I can use and manipulate that recorded audio in different components of the app.
My global state is setup using React Hooks (made and managed with useReducer, createContext, useContext) and I believe Hooks only work for functional components, not class components.
So the issue I'm running up against is that every tutorial I've followed to get my browser microphone to work uses class components (like the code below), not functional components. And I'm assuming that this is for good reason because when I've tried to translate these class components into functional components, I get the error: "cannot read property 'finish' of 'undefined'"
Are there ways to take this audio data (blobURL) and pass it to my global state?
Alternatively (and ideally), is there a way to use the microphone to record audio in a functional component instead of a class component?
import MicRecorder from "mic-recorder-to-mp3";
import React from "react";
const Mp3Recorder = new MicRecorder({ bitRate: 128 });
class AudioRecorder extends React.Component {
constructor(props) {
super(props);
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.state = {
isRecording: false,
isPaused: false,
blobURL: "",
isBlocked: false
};
}
startRecording = () => {
if (this.state.isBlocked) {
console.log("Please give permission for the microphone to record audio.");
} else {
Mp3Recorder.start()
.then(() => {
this.setState({ isRecording: true });
})
.catch(e => console.error(e));
}
};
stopRecording = () => {
this.setState({ isRecording: false });
Mp3Recorder.stop()
.getMp3()
.then(async ([buffer, blob]) => {
const blobURL = URL.createObjectURL(blob)
this.setState({
blobURL: blobURL,
isRecording: false
});
})
.catch(e => console.log(e));
};
checkPermissionForAudio = () => {
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function(constraints) {
// First get ahold of the legacy getUserMedia, if present
var getUserMedia =
// navigator.getUserMedia ||
navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// Some browsers just don't implement it - return a rejected promise with an error
// to keep a consistent interface
if (!getUserMedia) {
return Promise.reject(
new Error("getUserMedia is not implemented in this browser")
);
}
// Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
return new Promise(function(resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => {
this.setState({ isBlocked: false });
})
.catch(err => {
this.setState({ isBlocked: true });
console.log("Please give permission for the microphone to record audio.");
console.log(err.name + ": " + err.message);
});
};
componentDidMount() {
this.checkPermissionForAudio();
}
render() {
const { isRecording } = this.state;
return (
<React.Fragment>
<button
onClick={this.startRecording}
className="mr-3 add-collec-btn"
disabled={isRecording}
>
Record
</button>
<button
onClick={this.stopRecording}
className="mr-3 delete-btn"
disabled={!isRecording}
>
Stop
</button>
<audio
ref="audioSource"
controls="controls"
src={this.state.blobURL || ""}
/>
</React.Fragment>
);
}
}
export default AudioRecorder;
UPDATE:
This is how I've set up Context in my application and how it's provided through the code. In my store folder, I have three files: Context.js, GlobalStateProvider, and useGlobalState.
Context.js
import { createContext } from 'react';
const Context = createContext({});
export default Context;
GlobalStateProvider.js
This wraps everything in my App.js file
import React from 'react';
import useGlobalState from './useGlobalState';
import Context from './Context';
const GlobalStateProvider = ({ children }) => {
return (
<Context.Provider value={useGlobalState()}>{children}</Context.Provider>
);
}
export default GlobalStateProvider;
useGlobalState.js
import { useReducer } from 'react';
const reducer = (state, action) => {
switch (action.type) {
case 'SETISRECORD':
return {
...state,
isRecording: action.payload
}
case 'SETISBLOCKED':
return {
...state,
isBlocked: action.payload
}
case 'setBlobURL':
return {
...state,
blobURL: action.payload
}
default: {
return state;
}
}
};
const useGlobalState = () => {
const [globalState, globalDispatch] = useReducer(reducer, {
isRecording: false,
isBlocked: false,
blobURL: '',
});
return { globalState, globalDispatch };
}
export default useGlobalState;
I then interface with my global state in functional components like so:
const functionalComponent = () => {
const { globalState, globalDispatch } = useContext(Context);
return (
[code]
);
}
Your class-based components can still "consume" the context but the syntax is a little more involved than simply using a useContext React hook.
Context.Consumer
For your case you would import your global state context Context and render the component that needs to access the context via a function child. The child component would then need consume these context values via props.
Some classed-based component:
class MyComponent extends React.Component {
...
render() {
const { myContext: { globalState, globalDispatch } } = this.props;
return (
[code]
);
}
}
Wrap and pass via props:
import MyContext from '../path/to/context';
...
<MyContext.Consumer>
{myContext => <MyComponent myContext={myContext} />}
</MyContext.Consumer>
I created an HOC to deal with loading. It uses a property isLoading to decide what to show.
When I open a page directly from the URL I haven't any issue and everything works fine (because isLoading property is setted up on true by default).
The problem is that when I click on a link (<Link ... />) things doesn't work as expected because isLoading is now false because the page where I am navigating from setted the state to this value.
So, this is the HOC:
import React from 'react';
export default function WithLoading(WrappedComponent, loadingDelegate) {
class LoadingComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true
};
this.loadingDelegate = loadingDelegate;
}
componentDidMount() {
loadingDelegate(this);
}
render() {
if (this.props.isLoading === true) {
return (
<div>Loading...</div>
);
} else {
return <WrappedComponent {...this.props} />;
}
}
}
return LoadingComponent;
}
And below the component:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from './SurveysStore';
import WithLoading from '../LoadingHOC';
//edit/:id
class SurveyDetailRoutedComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.props.survey.questions.length}
</div>
);
}
}
const SurveyDetailRoutedComponentWithLoading = WithLoading(SurveyDetailRoutedComponent, (_context) => _context.props.requestSurvey(parseInt(_context.props.match.params.id)));
export default connect(
state => state.surveysReducer,
dispatch => bindActionCreators(actionCreators, dispatch)
)(SurveyDetailRoutedComponentWithLoading);
The error I have is that survey is null, because I tried to render a not already resolver property.
The problem occurs only when I render this component via a <Link> contained in a page rendered in the same way.
I tried to set isLoading=true on the routing, but this doesn't work for me:
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Layout>
<Route exact path='/' component={(props) => <Home {...props} />} />
<Route path='/survey/edit/:id' component={(props) => <SurveyDetailRoutedComponent {...props} isLoading={true} />} />
</Layout>
);
}
}
The store itself is very simple and I imagine my error is on how I deal with redux, because it looks like the routing operation is not resetting the loading.
const resetLoading = "RESET_LOADING";
const requestSurveys = "REQUEST_SURVEYS";
const receiveSurveys = "RECEIVE_SURVEYS";
const requestSurvey = "REQUEST_SURVEY";
const receiveSurvey = "RECEIVE_SURVEY";
const initialState = { surveys: [], isLoading: true, survey: null };
export const actionCreators = {
resetLoading: () => function (dispatch, getState) {
dispatch({ type: resetLoading })
},
requestSurveys: () => async function (dispatch, getState) {
dispatch({ type: requestSurveys });
const response = await fetch(...)
const responseAsJson = await response.json();
dispatch({ type: receiveSurveys, surveys: responseAsJson.data.surveys });
},
requestSurvey: id => async function (dispatch, getState) {
dispatch({ type: requestSurvey });
const response = await fetch(...)
const responseAsJson = await response.json();
dispatch({ type: receiveSurvey, survey: responseAsJson.data.survey });
}
};
export const reducer = function(state, action) {
state = state || initialState;
switch (action.type) {
case resetLoading:
return {
...state,
isLoading: true
};
case requestSurveys:
return {
...state,
isLoading: true
};
case requestSurvey:
return {
...state,
isLoading: true
};
case receiveSurveys:
return {
...state,
isLoading: false,
surveys: action.surveys
};
case receiveSurvey:
return {
...state,
isLoading: false,
survey: action.survey
};
default:
return state;
}
}
I am in need of guidance with getting through this error. The code is supposed to get the results from WebAPI while going through actions and services. In the actions is a dispatch where the error is. On my actions page it should call the service for WebAPI and depend on the response dispatch to the reducers for actions. The code does not pass the first dispatch in the jobActions.getjobs()
The error received from this is:
Unhandled Rejection (TypeError): _actions_job_actions__WEBPACK_IMPORTED_MODULE_1__.jobActions.getJobs(...).then is not a function
Page Load
import React from 'react';
import { jobActions } from '../../actions/job.actions';
class LoadTable extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
this.props.getJobs()
.then((res) => {
this.setState({ data: res.response || [] })
});
}
render() {
return ();
}
const mapDispatchToProps => dispatch => ({ getJobs: () => dispatch(jobActions.getJobs()) });
export default connect(mapDispatchToProps)( LoadTable );
===============================================
Actions
import { jobConstants } from '../constants/job.constants';
import { jobService } from '../services/job.service';
export const jobActions = {
getJobs
};
let user = JSON.parse(localStorage.getItem('user'));
function getJobs() {
return dispatch => {
dispatch(request());
return jobService.getJobs()
.then(
results => {
dispatch(success(user));
return { results };
},
error => {
dispatch(failure(error));
}
);
};
function request() { return { type: jobConstants.JOB_REQUEST }; }
function success(user) { return { type: jobConstants.JOB_SUCCESS, user }; }
function failure(error) { return { type: jobConstants.JOB_FAILURE, error }; }
}
=======================================================
services
export const jobService = {
getJobs
};
const handleResponseToJson = res => res.json();
function getJobs() {
return fetch('http://localhost:53986/api/jobs/getoutput')
.then(handleResponseToJson)
.then(response => {
if (response) {
return { response };
}
}).catch(function (error) {
return Promise.reject(error);
});
}
The result should be table data from the services page, actions page dispatching depending on the stage.
I assume you are using some sort of a middleware, like redux-thunk? If not, then your action creator returns a function, which is not supported by pure redux
I guess you do, because the error says that the action creator returned undefined after it was called
function getJobs() {
console.log("test -1");
return dispatch => {
console.log("test-2");
dispatch(request());
jobService.getJobs() // <==== here comes the promise, that you don't return
// return jobService.getJobs() <== this is the solution
.then(
results => {
console.log("test -3");
dispatch(success(user));
return { results };
},
error => {
dispatch(failure(error));
}
);
};
Update: you also need to map your action in mapDispatchToProps
Page Load
import React from 'react';
import { jobActions } from '../../actions/job.actions';
class LoadTable extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
this.props.getJobs() // as the name of mapDispatchToProps says, you mapped your action dispatch
// to a getJobs prop, so now you just need call it
.then((res) => {
this.setState({
data: res.response || []
})
}));
}
render() {
return ();
}
const mapStateToProps = state => ({});
const mapDispatchToProps = dispatch => ({
// this function will dispatch your action, but it also mapps it to a new prop - getJobs
getJobs: () => dispatch(jobActions.getJobs())
});
export default connect(mapStateToProps, mapDispatchToProps)( LoadTable );
I have a problem with displaying data.
In my application I use react and redux.
In the console I will get an error mapStateToProps() in Connect(ListPets) must return a plain object. Instead received undefined.
This is my main component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import loadData from '../actions/actions';
class ListPets extends Component {
componentDidMount() {
const { loadData } = this.props;
loadData();
console.log(loadData );
}
render() {
const { dataPet } = this.props;
return (
<div>
</div>
);
}
}
const mapStateToProps = (state) => {
return state;
};
const mapDispatchToProps = (dispatch) => {
return {
loadData: () => dispatch(loadData())
}
};
This fragment console.log(loadData ); display
ƒ loadData() {
return dispatch(Object(_actions_actions__WEBPACK_IMPORTED_MODULE_7__["default"])());
}
When I add the code {dataPet.data} in div. I get an error]. As if this data was not in the store, I do not know...
this my reducer function
const initialState = {
isFetching: false,
dataPet: [],
};
const fetchDataReducer = (state=initialState, action) => {
switch(action.types) {
case actionTypes.FETCH_DATA_START:
return {
...state,
isFetching: true,
}
case actionTypes.FETCH_DATA_SUCCESS:
return {
...state,
isFetching: false,
dataPet: action.dataPet,
}
case actionTypes.FETCH_DATA_FAIL:
return {
...state,
isFetching: false,
}
};
}
Data is well downloaded, because the console receives the FETCH_DATA_SUCCESS action.
I have no idea how to solve this problem
I made some changes on your code, try this now...should work
https://codesandbox.io/s/z2volo1n6m
In your reducer you have a typo:
const fetchDataReducer = (state=initialState, action) => {
switch(action.types) { // here
It should be action.type not action.types.
If thing is an object in state:
const mapStateToProps = state => ({
thing: state.thing,
});
Then use like:
this.props.thing in your component
I am developing a Web application using React + Redux. What I am doing now is I am making a API request using the Axios network library. I do not want to make the asyc API call in the component. As a book I am reading recommended, I am making the call in the actions.
This is my actions.
export const EVENT_START_FETCHING_LIST = "(EVENT) request list start";
export const EVENT_COMPLETE_FETCHING_LIST = "(EVENT) request list completed";
export const EVENT_THROW_ERROR_FETCHING_LIST = "(EVENT) request list throw error";
import { getHttpClient } from '../memento/getHttpClient';
export const startFetchingEvents = (data) => {
return (dispatch) => {
getHttpClient().get('event/list').then((response) => {
dispatch(completeFetchingEvents(response.data));
}).catch((error) => {
dispatch(throwErrorFetchingEvents({ message: "Error in fetching events from the server", styleClass: "alert alert-danger" }));
})
}
}
export const completeFetchingEvents = (data) => {
return {
type : EVENT_COMPLETE_FETCHING_LIST,
payload : data
}
}
export const throwErrorFetchingEvents = (data) => {
return {
type : EVENT_THROW_ERROR_FETCHING_LIST,
payload : data
}
}
If I want to make the call, I just trigger the action like this in the component.
this.props.startFetchingEvents()
The problem I am having is that updating the state. I have a property in the state, called "loading.". This is my reducer.
import * as EventListActions from '../actions/event.list.actions';
const DEFAULT_STATE = {
loading: false,
message: null,
events: []
}
export default function (state = DEFAULT_STATE, action) {
switch (action.type) {
case EventListActions.EVENT_START_FETCHING_LIST:
{
//TODO: this event is not triggerd
return { ...state, loading: true, events: [], message: null };
}
case EventListActions.EVENT_COMPLETE_FETCHING_LIST:
{
return { ...state, loading: false, events: action.payload };
}
case EventListActions.EVENT_THROW_ERROR_FETCHING_LIST:
{
return {
...state,
loading: false,
message: {
}
}
}
}
return state;
}
As you can see in my reducer, I am updating the loading value to true, when the API called is made. But as I commented, that function is not triggered. Other functions are triggered. What is wrong with my code and how can I fix it?
It seems simple. You forgot to dispatch it...
export const startFetchingEvents = (data) => {
return (dispatch) => {
dispatch(startFetchingEvents());
getHttpClient().get('event/list').then((response) => {
dispatch(completeFetchingEvents(response.data));
}).catch((error) => {
dispatch(throwErrorFetchingEvents({ message: "Error in fetching events from the server", styleClass: "alert alert-danger" }));
})
}
}
const startFetchingEvents = () => {
return {
type : EVENT_START_FETCHING_LIST
}
}
You are not dispatching the EVENT_START_FETCHING_LIST action type. You can do it like this:
export const startFetchingEvents = (data) => {
return (dispatch) => {
dispatch({
type : EVENT_START_FETCHING_LIST,
});
return getHttpClient().get('event/list').then((response) => {
dispatch(completeFetchingEvents(response.data));
}).catch((error) => {
dispatch(throwErrorFetchingEvents({ message: "Error in fetching events from the server", styleClass: "alert alert-danger" }));
})
}
}