I'm using React and Redux. I'm new to React, but not to development. This is my first independent project after training. I want to really learn this and I don't understand what is happening right now.
I have a Delete page that I route to for List Items. I pass in the prop of list_item_id via Route. I'm getting that prop with no problem, and can access it properly through my method. This page is also using Modal, in case that's a factor.
Here's the Route section from App.js
return(
<div className="ui container">
<Router history={history}>
<div>
<Header />
<Switch>
<Route path="/" exact component={List} />
<Route path="/lists" exact component={List} />
<Route path="/list/:list_id" exact component={ListItemList} list_id={this.props.list_id} />
<Route path="/listitems/new/:list_id" exact component={ListItemCreate} />
<Route path="/listitems/edit/:list_item_id" exact component={ListItemEdit} />
<Route path="/listitems/delete/:list_item_id" exact component={ListItemDelete} />
<Route path="/listitems/:list_item_id" exact component={ListItemShow} />
</Switch>
</div>
</Router>
</div>
);
I'm getting the error onclick of the Delete button.
The prop that is undefined I'm getting from Redux via mapStatetoProps:
const mapStateToProps = (state, ownProps) => {
return {
listItem: state.listItems[ownProps.match.params.list_item_id]
};
};
When the page initially renders, it has the correct value. But when I click the Delete button, for some reason it's re-rendering, listItem is undefined, and it breaks.
Here's the component:
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Modal from '../Modal';
import history from '../../history';
import { fetchListItem, deleteListItem } from '../../actions';
class ListItemDelete extends React.Component {
componentDidMount(){
this.props.fetchListItem(this.props.match.params.list_item_id);
}
renderActions(){
const list_item_id =this.props.match.params.list_item_id;
console.log('listItem');
console.log(this.props.listItem);
const list_id = this.props.listItem.list_id; // <--- breaking here
//console.log('list_id is ' + list_id);
return(
<React.Fragment>
<button onClick={() => this.props.deleteListItem(list_id, list_item_id)} className="ui button negative">Delete</button>
<Link to="/" className="ui button ">Cancel</Link>
</React.Fragment>
);
};
renderContent(){
if(!this.props.listItem){
return 'Are you sure you want to delete this list item?';
}
return `Are you sure you want to delete list item: ${this.props.listItem.list_item}?`;
}
render(){
return (
<Modal
title="Delete List Item"
content={this.renderContent()}
actions={this.renderActions()}
onDismiss={() => history.push("/")}
/>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
listItem: state.listItems[ownProps.match.params.list_item_id]
};
};
export default connect(mapStateToProps, { fetchListItem, deleteListItem })(ListItemDelete);
In case it's relevant, here's the action creator:
export const deleteListItem = (list_id, list_item_id) => async dispatch => {
let data = new FormData();
data.append('request', 'delete');
data.append('list_item_id', list_item_id);
await listItems.post(`/listItemsMaintenance`, data);
dispatch({ type: DELETE_LIST_ITEM, payload: list_item_id });
history.push(`/list/${list_id}`);
};
And the reducer (It's also not deleting properly, which is why I'm looking into it):
import _ from 'lodash';
import {
FETCH_LIST_ITEM,
FETCH_LIST_ITEMS,
CREATE_LIST_ITEM,
EDIT_LIST_ITEM,
DELETE_LIST_ITEM,
MARK_COMPLETED_LIST_ITEM
} from '../actions/types';
export default (state = {}, action) => {
const emptyObj = {};
switch (action.type) {
case FETCH_LIST_ITEMS:
return { ...state, ..._.mapKeys(action.payload, 'list_item_id') };
case FETCH_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case CREATE_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case EDIT_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case MARK_COMPLETED_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case DELETE_LIST_ITEM:
console.log('Now Reducing DELETE');
console.log(action.payload);
return _.omit(state, action.payload);
default:
return state;
}
};
And WHY is it calling the renderActions method so many times? Twice when it correctly gets the value of listItem and twice when it doesn't?? Here's an image of the error and console.
Related
I have a reactjs+redux app in which app.js, the first component to be mounted is given below:
//all imports here
class App extends React.Component {
constructor(){
super();
this.state = {
loginState : false,
}
}
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log(user);
if(user.displayName&&user.photoURL&&user.email)
this.props.dispatch(login(user.displayName, user.photoURL, user.email));
else
this.props.dispatch(login(user.email.split("#")[0], "", ""));
this.setState({
loginState: true
});
}
else{
this.props.dispatch(changeLoading());
}
});
}
logout = () => {
firebase
.auth()
.signOut()
.then(() => {
this.setState({
loginState : false,
});
this.props.dispatch(logout());
this.props.history.push('/login');
})
.catch((err) => {
console.log(err);
});
};
render() {
return (
<>
<Switch>
{this.props.userName?(<Route
exact
path="/"
component={() => <Homepage userName={this.props.userName} />}
/>):(<Route exact path="/" component={Loading} />)}
<Route exact path="/login" component={Login} />
<Redirect to="/" />
</Switch>
</>
);
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.userState.isLoggedIn,
userName: state.userState.userName,
email: state.userState.email,
photoURL: state.userState.photoURL
};
};
export default withRouter(connect(mapStateToProps)(App));
Below is Homepage.js:
class Homepage extends React.Component{
componentDidMount(){
console.log("component mounted");
this.props.dispatch(fetchPosts());
}
render(){
console.log("Homepage reached");
if(this.props.userName==='') return <Redirect to="/login" />
return(
<div className="container-fluid m-0" style={{paddingTop: '100px',paddingLeft: '0',paddingRight: '0'}}>
<div className="row m-0">
<div class="col-md-1"></div>
<Main userName={this.props.userName} />
<Leftside userName={this.props.userName} />
<div class="col-md-1"></div>
</div>
</div>
);
}
}
const mapStateToProps=(state)=>({})
export default connect(mapStateToProps)(Homepage);
And below is the reducer:
export const userState=(state={isLoading: true,isLoggedIn: false,userName: '', photoURL: ''},action)=>{
switch(action.type){
case 'LOGIN': {console.log("reached");return {...state,isLoading: false,isLoggedIn: true, userName: action.payload.userName, photoURL: action.payload.photoURL, email: action.payload.email}}
case 'LOGOUT': return {...state,isLoading: false,isLoggedIn:false,userName: '',photoUrl: ''}
case 'CHANGE': return {...state,isLoading: false}
default: return {...state}
}
}
Basically what is happening is that initially when the app opens, this.props.userName is empty and hence Loading component is loaded. Once the firebase returns the user details, they are dispatched to redux reducer. When this happens, state.userState.userName becomes available and Homepage is mounted. As expected, its componentDidMount method runs and posts are fetched and dispatched to the redux store( posts are stored in redux store). But then suddenly Homepage unmounts and mounted again and consequently, componntDidMount runs again. So, in total, there are two fetchPost requests.
I do not understand this behaviour. I have read that componentDidMount runs only a single time.
Please help me to remove this bug.
Thank You!
My Home.js component just doesn't seem to see the dispatch function at all. Do you guys know why? I'm kinda new to redux style state management stuff in redux.
I keep getting the error "TypeError: dispatch is not a function"
App.js
import React from 'react';
import { HashRouter, Route } from 'react-router-dom';
import Home from './pages/Home';
import Start from './pages/Start';
import Result from './pages/Result';
import RPSContextProvider from './contexts/RPSContext';
const App = () => {
return (
<HashRouter>
<RPSContextProvider>
<Route exact path="/" component={Home} />
<Route path="/start" component={Start} />
<Route path="/result" component={Result} />
</RPSContextProvider>
</HashRouter>
);
};
export default App;
Home.js
import React, { useRef, useContext } from 'react';
import { RPSContext } from '../contexts/RPSContext';
import './home.css';
const Home = (props) => {
const { state, dispatch } = useContext(RPSContext);
const playerNameEntry = useRef();
const handleClick = () => {
if (!isStringEmpty(playerNameEntry.current.value)) {
dispatch({ type: 'SET_NAME', state: playerNameEntry.current.value });
props.history.push({
pathname: '/start'
});
console.log(dispatch);
}
};
const isStringEmpty = (string) => string.trim().length === 0;
return (
<div className="app-container">
<h1>
You dare battle me at
<br />
Rock, Paper, Scissors?
<br />
You got no chance, kid!
</h1>
<p>What's your name, ya chancer?</p>
<input type="text" onKeyPress={(e) => handleKeyPress(e)} ref={playerNameEntry} />
<button onClick={handleClick}>Start</button>
</div>
);
};
export default Home;
RPSContext.js
import React, { createContext, useReducer } from 'react';
import { RPSReducer } from '../reducers/RPSReducer';
export const RPSContext = createContext();
const RPSContextProvider = (props) => {
const [ state, dispatch ] = useReducer(RPSReducer, { playerName: '' });
return <RPSContext.Provider value={{ state, dispatch }}>{props.children}</RPSContext.Provider>;
};
export default RPSContextProvider;
RPSReducer.js
export const RPSReducer = (state, action) => {
switch (action.type) {
case 'SET_NAME':
return { playerName: action };
default:
throw new Error();
}
};
Basically as a first step I just want to set the name of the entry. I know this is quite a lot of code just for what I'm doing, but just wanting to try out useReducer and useContext so that I can learn all this new stuff in React.
I solved the problem by adding
switch (action.type) {
case 'SET_NAME':
return { ...state, playerName: action.payload }
in my reducer, and in Home.js changed the state key I had in there to payload. Not 100% sure if it having the same name was effecting anything, but its much less confusing naming it payload.
const handleClick = () => {
if (!isStringEmpty(playerNameEntry.current.value)) {
dispatch({ type: 'SET_NAME', payload: playerNameEntry.current.value });
Wrap the whole App with AppContext.Provider passing with state and dispatch, like below
<AppContext.Provider value={{ state, dispatch }}>
<div className="App">
<Compo />
</div>
</AppContext.Provider>
I am new in react and redux. I am fetching data from one page page and I would like to show details of the record. When I click from the list to see details for the first time, it is fine. But when I would like to see the next result details and for a millisecond there is a blink of previous result and then re rendered by the new one. Could you help me with this issue, please? I do not want to see the previous artist detail.
Show details
import React from 'react';
import { connect } from 'react-redux';
import { fetchArtistDetails } from '../../actions';
class ResultDetails extends React.Component{
componentDidMount(){
this.props.fetchArtistDetails(this.props.match.params.id);
}
renderList(){
return this.props.artist.map(artist => {
return(
<div className="ui segment">
<br/>
<div className="ui two column centered grid">
<div className="ui centered massive label">{artist.name}</div>
</div>
<br/>
<br/>
{ (artist.images) ?
artist.images.map(image =>{
return image.type ==='primary' ? <img className="ui centered medium image" src={image.uri}/> : null
}) : null
}
<div className="ui small images">
{ (artist.images) ?
artist.images.map(image =>{
return image.type ==='secondary' ? <img src={image.uri}/> : null
}) : null
}
</div>
<p>{artist.profile}</p>
</div>
);
});
}
render(){
if (!this.props.artist) {
console.log("ahoj");
return <div>Loading...</div>;
}
return(
<div>
<div>{this.renderList()}</div>
</div>
);
}
}
const mapStateToProps = state => {
return { artist: state.artistDetails }
}
export default connect( mapStateToProps , { fetchArtistDetails }) (ResultDetails);
Actions
import discogs from '../apis/discogs';
import history from '../history';
import { FETCH_POSTS, SEARCH_ARTIST, FETCH_ARTIST_DETAILS, CLEAN_ARTIST_DETAILS } from './types';
export const fetchPosts = text => async dispatch => {
const response = await discogs.get(`/database/search?q=${text}&type=artist`);
dispatch({ type: FETCH_POSTS, payload: response.data.results });
history.push(`/results/${text}`);
};
export const searchArtist = text => dispatch => {
dispatch({
type: SEARCH_ARTIST,
payload: text
});
};
export const fetchArtistDetails = id => async dispatch => {
const response = await discogs.get(`/artists/${id}`);
dispatch({ type: FETCH_ARTIST_DETAILS, payload: response.data });
history.push(`/details/${id}`);
};
Reducer
import {
FETCH_ARTIST_DETAILS,
} from '../actions/types';
export default (state = [], action) => {
switch (action.type) {
case FETCH_ARTIST_DETAILS:
return [action.payload]
default:
return state;
}
};
App
import React, { Fragment } from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import MainPage from '../pages/MainPage';
import SearchResult from '../components/searchResult/SearchResult';
import ResultDetails from '../components/resultDetails/ResultDetails';
import Header from './header/Header';
import history from '../history';
const App = () => {
return (
<div className="ui container">
<Router history={history}>
<div>
<Switch>
<Route exact path='/' component={MainPage} />
<Fragment>
<Header />
<Route exact path="/results/:id" component={SearchResult} />
<Route exact path="/details/:id" component={ResultDetails} />
</Fragment>
</Switch>
</div>
</Router>
</div>
);
};
export default App;
I found and issue (I do not know, if it is proper solution but it works for me)
New action
export const cleanArtistDetails = () => async dispatch => {
dispatch({ type: CLEAN_ARTIST_DETAILS, payload: null });
};
Update reducer
import {
FETCH_ARTIST_DETAILS, CLEAN_ARTIST_DETAILS,
} from '../actions/types';
export default (state = [], action) => {
switch (action.type) {
case FETCH_ARTIST_DETAILS:
return [action.payload]
case CLEAN_ARTIST_DETAILS:
return null
default:
return state;
}
};
And update component
componentWillUnmount(){
this.props.cleanArtistDetails();
}
Maybe I am missing something obvious here but I have a Sidebar component that is used on 2 pages of my app; Blog and Post. In my Sidebar component I have a dispatch to fetch some data for "pinned articles" and in my App I have some routes. When I click on the link in the sidebar, the link works but if I click a different link in the sidebar, nothing happens. In my redux store I have enabled logging middleware and I see the first click on the link is SUCCESS but clicking a second link in the sidebar shows nothing but CLEAR. Any suggestions?
App.jsx
[... unrelated code omitted ...]
render() {
const { alert } = this.props;
return (
<div>
{alert.message &&
<div className={`alert ${alert.type}`}>{alert.message}</div>
}
<Router history={history}>
<div>
<PrivateRoute exact path="/" component={HomePage} />
<PrivateRoute exact path="/resource-centre" component={Blog} />
<PrivateRoute path="/resource-centre/:id" component={Post} />
<Route path="/login" component={LoginPage} />
<Route path="/register" component={RegisterPage} />
</div>
</Router>
</div>
);
}
Sidebar.jsx
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { articleActions } from '../../actions';
import _ from 'lodash';
class Sidebar extends React.Component {
componentDidMount() {
this.props.dispatch(articleActions.fetchPinnedArticles());
}
render() {
const { pinnedArticles } = this.props;
return (
<div>
<h2 className="widget-title">Pinned</h2>
<ul>
{pinnedArticles.items && pinnedArticles.items.map(article =>
<li className="tag-cloud-link" key={article.id}>
<Link to={{ pathname: `/blog/${article.id}` }}>{article.title}</Link>
</li>
)}
</ul>
</div>
)
}
}
function mapStateToProps(state) {
const { pinnedArticles } = state;
return {
pinnedArticles
}
}
const connectedSidebar = connect(mapStateToProps)(Sidebar);
export { connectedSidebar as Sidebar };
articleAction.js
function fetchPinnedArticles() {
return dispatch => {
dispatch(request());
articleService.fetchPinnedArticles()
.then(
pinnedArticles => dispatch(success(pinnedArticles)),
error => dispatch(failure(error))
);
};
function request() {
return { type: articleConstants.FETCH_PINNED_REQUEST }
}
function success(pinnedArticles) {
return { type: articleConstants.FETCH_PINNED_SUCCESS, pinnedArticles }
}
function failure(error) {
return { type: articleConstants.FETCH_PINNED_FAILURE, error }
}
}
articleService.js
function fetchPinnedArticles() {
const requestOptions = {
method: 'GET',
headers: authHeader()
};
return fetch(`${config.apiUrl}/articles/pinned`, requestOptions).then(handleResponse, handleError);
}
articleReducer.js
export function pinnedArticles(state = {}, action) {
switch (action.type) {
case articleConstants.FETCH_PINNED_REQUEST:
return {
loading: true
}
case articleConstants.FETCH_PINNED_SUCCESS:
return {
items: action.pinnedArticles
};
case articleConstants.FETCH_PINNED_FAILURE:
return {
error: action.error
};
default:
return state;
}
}
I am trying to make a login page which will redirect to the home page after successful authentication. Here is the code.
routes.js
const createRoutes = (store, history) => {
return (
<Router history={history}>
<div>
<AppLayout/>
<Route exact path="/" component={Home}/>
<Route path="/login" component={LoginContainer}/>
<Route path="/register" component={RegisterContainer}/>
</div>
</Router>
);
};
export default createRoutes;
actions.js
export const login = (data, successPath) => {
return (dispatch) => {
dispatch(beginLogin());
return makeUserRequest("post", "/login", data)
.then((resp) => {
if (resp.data.success) {
dispatch(loginSuccess(data));
browserHistory.push(successPath);
} else {
dispatch(loginError(resp.data));
}
})
.catch(console.error);
};
};
Login.js
class Login extends React.Component {
login = (event) => {
event.preventDefault();
const email = event.target.querySelector("input[name='email']").value;
const password = event.target.querySelector("input[name='password']").value;
this.props.login({
email,
password
}, this.props.nextPathName);
}
render() {
return (
<div className="Login">
<h2>Login</h2>
<form onSubmit={this.login}>
<input type="email" name="email" placeholder="Email"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Login"/>
<h1>{this.props.user.email}</h1>
</form>
</div>
);
}
}
Login.propTypes = {
login: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
nextPathName: PropTypes.string.isRequired
};
export default Login;
LoginContainer.js
const mapStateToProps = (state, ownProps) => {
let nextPathName = "/";
try {
nextPathName = ownProps.location.state.nextPathName;
} catch(err) {
// ignore
}
return {
user: state.user,
nextPathName
};
};
export default withRouter(connect(mapStateToProps, userActions)(Login));
reducer.js
// reducer for user actions
const user = (state = { isWaiting: false, authenticated: false, email: "", message: "" }, action) => {
switch(action.type) {
case REGISTER_USER:
return { ...state, isWaiting: true };
case REGISTER_SUCCESS_USER:
return { ...state, isWaiting: false, message: action.data.message };
case REGISTER_ERROR_USER:
return { ...state, isWaiting: false, message: action.data.message };
case LOGIN_USER:
return { ...state, isWaiting: true };
case LOGIN_SUCCESS_USER:
return { ...state, isWaiting: false, authenticated: true, email: action.data.email };
case LOGIN_ERROR_USER:
return { ...state, isWaiting: false };
default:
return state;
}
};
export default user;
export default combineReducers({
user,
routing: routerReducer
});
The login functionality is working correctly and when I am clicking the login button, the email in the component is printing correctly. After that the browser is changing to the new location which is "/", but the page is not being rendered, i.e. the view is remaining same even after the browser location has changed. I have looked into similar question answers on stackoverflow, but have not found any solution to my problem. I have even used withRouter but to no solution. Any help will be appreciated.
I think you are supposed to use one of the higher level Router components like : <BrowserRouter> if your web/app is hosted on a dynamic server
or <HashRouter> if your webs/app is hosted on a static server
routes.js
import {BrowserRouter, Route} from 'react-router-dom';
const createRoutes = (store, history) => {
return (
<BrowserRouter>
<div>
<AppLayout/>
<Route exact path="/" component={Home}/>
<Route path="/login" component={LoginContainer}/>
<Route path="/register" component={RegisterContainer}/>
</div>
<BrowserRouter>
);
};
export default createRoutes;
<BrowserRouter> is just a wrapper with a pre-built history so you can do same with custom history instance . You can read more Here
Instead of using the imperative push method, you could use the nice declarative <Redirect /> component that comes with React Router:
{user.authenticated && <Redirect to={nextPath} />}
You can place this wherever it makes sense for you. Inside your Login component or at the top level (just making sure not to redirect over and over)