I'm building a Dashboard style app that would show data service outages. The backend is Rails 6 and I'm using React/Redux in the frontend (within Rails). I'm having lots of trouble (due to my greenness in Redux) with getting the data into the front end and mapping state to props. Would love for someone to look at my app and see where I'm going wrong. It seems like I'm also having issues with Lexical behaviour as well.
Here is the top of the app:
import React from 'react';
import { render } from 'react-dom'
import Dashboard from './Dashboard';
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from 'redux'; // we get our store from redux library and we need middleware to wire up Thunk
import thunk from 'redux-thunk';
import reducers from './reducers/rootReducer';
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap/dist/css/bootstrap.min.css";
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, storeEnhancers(applyMiddleware(thunk)));
// this is how you hook up
store.subscribe(() => {
console.log('the new state is', store.getState());
console.log('----------');
});
render(
<Provider store={store}>
<Dashboard />
</Provider>,
document.body.appendChild(document.createElement('div')),
)
This is the top visible component Dashboard.js
import React, { Component } from "react";
import RecurringOutagesContainer from "./containers/RecurringOutagesContainer";
import FutureOutagesContainer from "./containers/FutureOutagesContainer";
import CurrentOutagesContainer from "./containers/CurrentOutagesContainer";
import CreateModalComponent from "./components/CreateModalComponent";
import { Container, Row, Col, Image } from "react-bootstrap";
import { getFutureOutages } from "./actions/fetchFutureOutagesAction";
import { getRecurringOutages } from "./actions/fetchRecurringOutagesAction";
import { getServices } from "./actions/fetchServicesAction";
import { connect } from 'react-redux';
class Dashboard extends Component {
state = {
services: [],
outages: [],
showModal: false
};
componentDidMount() {
this.props.getFutureOutages()
this.props.getRecurringOutages()
this.props.getServices()
}
render() {
console.log(this.props)
return (
<div>
<Container>
<Row>
<Col sm={1}>
<img
src={require("./public/logo-2-dashboard.png")}
alt="logo"
id="logo"
></img>
</Col>
<Col md={8}></Col>
</Row>
</Container>
<div className="container">
<div className="d-flex justify-content-md-end bd-highlight">
</div>
</div>
<div className="d-flex justify-content-center bd-highlight dashboard">
<div className="d-flex justify-content-start bd-highlight">
<div className="d-fliex pastOutages">
<h4>Past Outages</h4>
</div>
</div>
<div className="d-flex justify-content-center bd-highlight">
<div className="d-fliex currentOutages">
<h4>Current Outages</h4>
<div className="container">
<div className="col-12">
<CurrentOutagesContainer currentOutages={this.props.services} />
</div>
</div>
</div>
</div>
<div className="d-flex align-items-center flex-column bd-highlight">
<div className="d-fliex justify-content-center">
<h4>Future Outages</h4>
<div className="container" id="futureOutages">
<div className="col-12">
<FutureOutagesContainer futureOutages={this.props.futureOutages} />
</div>
</div>
<h4>Recurring Outages</h4>
<div className="container" id="recurringOutages">
<div className="col-12">
<RecurringOutagesContainer recurringOutages={this.props.recurringOutages} />
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
futureOutages: state.futureOutages,
recurringOutages: state.recurringOutages,
services: state.services
}
};
const mapDispatchToProps = dispatch => {
return {
getFutureOutages: () => dispatch(getFutureOutages()),
getRecurringOutages: () => dispatch(getRecurringOutages()),
getServices: () => dispatch(getServices())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); // this connects Dashboard to store
Here is one example of an action file:
\\ fetchFutureOutagesAction.js
import axios from 'axios';
export const getFutureOutages = () => dispatch => {
axios.get("/future_outages")
.then(res => {
const futureOutages = res.data;
dispatch({ type: 'FUTURE_OUTAGES', payload: futureOutages });
})
.catch(res => console.log(res.errors));
};
I have a rootReducer like so:
import { combineReducers } from 'redux';
import { futureOutagesReducer } from './futureOutagesReducer';
import { recurringOutagesReducer } from './recurringOutagesReducer';
import { servicesReducer } from './servicesReducer';
export default combineReducers({
futureOutages: futureOutagesReducer,
recurringOutages: recurringOutagesReducer,
services: servicesReducer
});
and here is an example of a reducer file:
const initialState = {
futureOutages: []
}
export const futureOutagesReducer = (state = initialState, action) => {
switch (action.type) {
case 'FUTURE_OUTAGES':
return { futureOutages: [...state.futureOutages, action.payload] };
default:
return state;
}
}
The errors are occuring in the container files that I pass props down to from `Dashboard.jsx':
import React from "react";
import FutureOutagesComponent from "../components/FutureOutagesComponent"
const FutureOutagesContainer = props => {
return (
<div>
{props.futureOutages && props.futureOutages.map((futureOutage, idx) => (
<FutureOutagesComponent key={idx} futureOutage={futureOutage} />
))
}
</div>
)
};
export default FutureOutagesContainer;
When I start ./bin/webpack-dev-server.js, here is a snapshot of errors in console I'm getting:
So clearly the props are not being passed down correctly. Can someone give me some pointers on implementing this better? I had everything working with just a React app but really want to have more flexibility accessing state through out the app.
Based on your reducer, I think you will need to access future outages like this.
const mapStateToProps = state => {
return {
futureOutages: state.futureOutages.futureOutages
}
};
The name of your reducer is futureOutages and it also has a property by the same name whose value is an array.
export default combineReducers({
// state.futureOutages
futureOutages: futureOutagesReducer,
...
})
Accessing state.futureOutages will give you an object which is the full piece of state for that reducer from the Refux store. But you want a specific property. Because it's an object and not array, Array.prototype.map is not a func. HTH.
Related
This is hard to explain without uploading my full project likely, but here goes. I think I've narrowed it down to some combination of getInitialProps() and getStaticProps(). When I use next/link to change pages images are not being loaded. If I browse directly to the page images will load fine. Project is fairly simple with only 2 pages, index.js and [slug].js. Here's both:
index.js
import React from 'react';
import Layout from '../components/layout';
import Seo from '../components/seo';
import Hero from '../components/hero';
import Forcast from '../components/forcast';
import { fetchAPI } from '../lib/api';
import ReactMarkdown from 'react-markdown';
const Home = ({ pages, homepage }) => {
return (
<Layout pages={pages}>
<Seo seo={homepage.seo} />
<Hero hero={homepage.hero} />
<Forcast />
<main className='main-content'>
<div className='fullwidth-block'>
<div className='container'>
<div className='post single'>
<div className='entry-content'>
<ReactMarkdown
source={homepage.Content}
escapeHtml={false}
transformImageUri={uri =>
uri.startsWith('http') ? uri : `${process.env.REACT_APP_IMAGE_BASE_URL}/${uri}`
}
/>
</div>
</div>
</div>
</div>
</main>
</Layout>
);
};
export async function getStaticProps() {
// Run API calls in parallel
const [pages, homepage] = await Promise.all([
fetchAPI('/pages'),
fetchAPI('/homepage'),
]);
return {
props: { pages, homepage },
revalidate: 1,
};
}
export default Home;
[slug].js
import ReactMarkdown from 'react-markdown';
import Layout from '../components/layout';
import Seo from '../components/seo';
import { fetchAPI } from '../lib/api';
const Page = ({ page, pages }) => {
const seo = {
metaTitle: page.Title,
metaDescription: page.seo.metaDescription,
shareImage: page.seo.shareImage,
}
return (
<Layout pages={pages}>
<Seo seo={page.seo} />
<main className='main-content'>
<div className='container'>
<div className='breadcrumb'>
</div>
</div>
<div className='fullwidth-block'>
<div className='container'>
<div className='row'>
<div className='content col-md-8'>
<div className='post single'>
<h2 className='entry-title'>{page.Title}</h2>
<ReactMarkdown
source={page.Content}
escapeHtml={false}
transformImageUri={uri =>
uri.startsWith('http') ? uri : `${process.env.REACT_APP_IMAGE_BASE_URL}${uri}`
}
/>
</div>
</div>
</div>
</div>
</div>
</main>
</Layout>
);
};
export async function getStaticPaths() {
const pages = await fetchAPI('/pages');
return {
paths: pages.map((page) => ({
params: {
slug: page.slug,
},
})),
fallback: false,
};
}
export async function getStaticProps({ params }) {
const pages = await fetchAPI(
`/pages?slug=${params.slug}`
);
return {
props: { page: pages[0], pages },
revalidate: 1,
};
}
export default Page;
This might also be a Strapi issue though I'm not sure.
The issue happens because the REACT_APP_IMAGE_BASE_URL is not exposed to the browser, and only available on the server.
To have it exposed to the browser you'll need to add the NEXT_PUBLIC_ prefix to it.
# .env.development
NEXT_PUBLIC_REACT_APP_IMAGE_BASE_URL=http://localhost:1337
Then in your code reference it using process.env.NEXT_PUBLIC_REACT_APP_IMAGE_BASE_URL.
I'm trying to set up a drag&drop component to upload multiple files. However, when I attempt to access elements on the DOM with the querySelector method, I end up with null.
I've tried to implement the AfterViewInit class to no avail. Here's my current dart code for the component:
import 'dart:html';
import 'package:dnd/dnd.dart';
import 'package:angular/angular.dart';
#Component(
selector: 'upload',
templateUrl: 'upload.html',
styleUrls: [
'upload.css'
]
)
class Upload implements AfterViewInit {
#override
void ngAfterViewInit() {
// TODO: implement ngOnInit
Draggable d = new Draggable(document.querySelectorAll('.page'), avatarHandler : new AvatarHandler.clone());
var del = document.querySelector('.upload');
print(del); //prints null
Dropzone dropzone = new Dropzone(document.querySelector('.upload')); //throws an error, as it doesn't expect null.
dropzone.onDrop.listen((DropzoneEvent event){
print(event);
});
}
}
Also, my upload.html file is as follows:
<div class="center-me page" uk-grid>
<div class="uk-align-center text-align-center">
<h2 class="text-align-center" >Upload a file</h2>
<div class="upload uk-placeholder uk-text-center">
<span uk-icon="icon: cloud-upload"></span>
<span class="uk-text-middle">Attach binaries by dropping them here or</span>
<div uk-form-custom>
<input type="file" multiple>
<span class="uk-link">selecting one</span>
</div>
</div>
<progress id="progressbar" class="uk-progress" value="0" max="100" hidden></progress>
</div>
</div>
Thanks in advance.
So this looks like it should work. I wouldn't actually suggest doing it this way as it will get any element with an upload class which if you reuse the component will be a lot.
I would suggest using the ViewChild syntax instead
class Upload implements AfterViewInit {
#ViewChild('upload')
void uploadElm(HtmlElement elm) {
Dropzone dropzone = new Dropzone(elm);
dropzone.onDrop.listen((DropzoneEvent event){
print(event);
});
}
}
In the template:
<div class="uk-placeholder uk-text-center" #upload>
That said you shouldn't be getting null from the querySelect, but from the code you have shown I'm not sure why.
I'm using React on Rails and currently logging out using a ERB component. I'm creating a hamburger menu for the app and putting the logout in it. Currently it just sitting out in the opening using <%= link_to "Logout", destroy_user_session_path, method: :delete %> in Rails. I would like to put it in the React component. Please help. I'm still new at using React on Rails. The code for this is below.
import React, { Component } from 'react'
class MenuContent extends Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="menu">
<div className="Logout">
I'm trying to add the JavaScript code here. Here how I'm doing it in Ruby.
<%= link_to "Logout", destroy_user_session_path, method: :delete %>
</div>
<p className="hint">Click outside the menu to close it, or swipe it closed on touch device</p>
</div>
)
}
}
export default MenuContent
This^ is being imported to here(below). It works with the rest of the code.
import React, {Component} from 'react'
import CheeseburgerMenu from "cheeseburger-menu";
import HamburgerMenu from "react-hamburger-menu";
import MenuContent from "./MenuContent";
class Navbar extends Component {
constructor(props) {
super(props);
this.state = {
menuOpen: false
};
}
openMenu() {
this.setState({ menuOpen: true });
}
closeMenu() {
this.setState({ menuOpen: false });
}
render(){
return(
<div>
<CheeseburgerMenu
isOpen={this.state.menuOpen}
closeCallback={this.closeMenu.bind(this)}>
<MenuContent closeCallback={this.closeMenu.bind(this)} />
</CheeseburgerMenu>
<HamburgerMenu
isOpen={this.state.menuOpen}
menuClicked={this.openMenu.bind(this)}
width={32}
height={24}
strokeWidth={3}
rotate={0}
color="black"
borderRadius={0}
animationDuration={0.5} />
</div>
)
}
}
export default Navbar
I got the log out working using this code. Thank you all who responded.
import React, { Component } from 'react'
import axios from 'axios'
// import './menuContent.scss'
class MenuContent extends Component {
constructor(props) {
super(props)
}
handleLogout = () => {
const link = document.createElement('a');
link.setAttribute('href', '/users/sign_out');
link.setAttribute('rel', 'nofollow');
link.setAttribute('data-method', 'delete');
document.body.appendChild(link);
link.click();
}
render() {
return (
<div className="grandMenu">
<div className="menu">
<div className="signButton"></div>
<button onClick={this.handleLogout}>Sign Out</button>
<p>hello world</p>
</div>
<p className="hint">
Click outside the menu to close it, or swipe it away.
</p>
</div>
)
}
}
export default MenuContent
P.S. I should of mention this earlier. The log in is though a API and using Devise.
You can actually log out through client-side.
All rails session does was setting browser cookies. You check that by opening console and type
document.cookie
If you're logged in, you might see something like
"auth_token=icHQZ7QB5WK1PPXCWIiF0A"
auth_token is the cookie name. In order to delete this cookie, you will need to set its expiry date to the past:
document.cookie = 'auth_token=;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Replace 'auth_token' with whatever cookie name you're using.
If that works, that means on your React, you simply do something like
import React, { Component } from 'react'
class MenuContent extends Component {
onLogout = () => {
document.cookie = 'auth_token=;expires=Thu, 01 Jan 1970 00:00:01 GMT;'
}
render() {
return (
<div className="menu">
<div onClick={this.onLogout} className="Logout">
Click to Log Out
</div>
<p className="hint">
Click outside the menu to close it, or swipe it closed on touch device
</p>
</div>
)
}
}
export default MenuContent
I have 2 problems regarding the below code. I want to remove specific items from my cart in React. ( backend Rails). I know that the splice approach is one of the most used but in my case, it does not delete the specific clicked one, but it deletes the last item all the time regardless of the one that I click one.
My second problem is that my Total does not get updated even if the items do get deleted in the wrong way but still get deleted.
This is the code:
import React, { Component } from 'react';
import BasketPic from './images/cart.png';
import StripePayment from './stripePayment';
class MainBasket extends React.Component {
constructor(props) {
super(props);
this.state = {
toggle: true,
showBasket: false,
items: this.props.items
}
this.toggleBasket = this.toggleBasket.bind(this)
this.removeItemFromBasket = this.removeItemFromBasket.bind(this)
console.log(this.state.items)
}
toggleBasket(){
this.setState({toggle: !this.state.toggle});
}
showCheckout(){
this.setState({showBasket: !this.state.showBasket})
}
addItemToBasket(itemId){
fetch(`http://localhost:3000/books/${itemId}`)
.then( item => item.json())
.then( item => {
this.state.items.push(item);
this.state.total += item.price;
this.setState(this.state);
})
}
removeItemFromBasket(itemId){
var itemToBeDeleted = this.state.items.indexOf(itemId)
var deleted = this.state.items.splice(itemToBeDeleted, 1)
this.setState(this.state)
}
render(){
var count_items = this.props.items.length
var total = this.props.total
return(
<div className="basketInfo">
<div className="mainBasketDiv">
<img src={BasketPic} onClick={this.toggleBasket} />
<strong><p className="itemsCart">{count_items}</p></strong>
<strong><p className="checkOutConf">CheckOut</p></strong>
<div className={"toggleDiv-" + this.state.toggle}>
<h3>{"Total: " + this.props.total}</h3>
<span><h4>{"Items: " + count_items }</h4></span>
<hr/>
{this.props.items.map( item =>
<div className="animated fadeInRight" key={"item-" + item.id}>
<h2>{item.title}</h2>
<h6 onClick={this.removeItemFromBasket} className="remvProd">{"Remove " + item.title}</h6>
</div>
)}
<button onClick={function(){this.showCheckout()}.bind(this)}> Check out</button>
</div>
</div>
<div className="container">
<div className={"checkOutStripe-" + this.state.showBasket}>
<div className="totalBar">
<p className="totalBarTypography"> Total {this.props.items.length} {this.props.items.length < 2 ? "item" : "items"}</p>
<p className="totalBarTypography"> Total {this.props.total} GBP</p>
</div>
<div className="row">
{this.props.items.map(eachItem =>
<div className="col-sm" key={"eachItem-" + eachItem.id}>
<img src={eachItem.image.url} className="checkOutImg"/>
<div className="prodDetails">
<h3>{"Product: " + eachItem.title }</h3>
<h3>{"Price: " + eachItem.price }</h3>
</div>
</div>
)}
</div>
<div>
<StripePayment
description={'BooksMania'}
amount={total}
/>
</div>
</div>
</div>
</div>
)
}
}
// module.exports = MainBasket;
export default MainBasket;
and this is the code where i set my total and my items initial state:
import React, { Component } from 'react';
import BuyButton from './images/buyButton.jpg';
import MainBasket from './mainBasket';
import CheckOutBasket from './checkOutBasket';
import Reviews from './reviews';
class EachBook extends React.Component {
constructor(props){
super(props);
this.state = {
newReview: [],
items: [],
total: 0
}
}
seeAllReviews(bookId){
fetch(`http://localhost:3000/books/${bookId}/reviews`)
.then( reviews => reviews.json())
.then( reviews => {
this.setState({
bookReviews: reviews
})
})
}
addReview(bookId){
fetch(`http://localhost:3000/books/${bookId}/reviews`,{
method: 'POST',
mode: 'cors',
body: JSON.stringify({comment: this.refs.comment.value}),
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token' : this.props.parent.props.parent.props.csrf
}),
credentials: 'same-origin'
}).then(response => response.json())
.catch(error => alert("There is something wrong with this request"))
.then( response => {
this.setState({newReview: response})
})
}
addItemToBasket(itemId){
fetch(`http://localhost:3000/books/${itemId}`)
.then( item => item.json())
.then( item => {
this.state.items.push(item);
this.state.total += item.price;
this.setState(this.state);
})
}
render(){
return(
<div>
<MainBasket items={this.state.items} total={this.state.total} parent={this}/>
<div className="container">
{this.props.singleBook.map(indBook =>
<div className="indBook" key={indBook.id}>
<h1>{indBook.title}</h1> <br />
<h2>{"Author: " + indBook.author}</h2> <br />
<h4>{"Genre: " + indBook.genre}</h4>
<h4>{"Price: " + indBook.price}£</h4>
<img src={indBook.image.url} />
<div className="button"><img src={BuyButton} onClick={function(){this.addItemToBasket(indBook.id)}.bind(this)}/></div>
<div className="description">{indBook.description}</div>
<h3>{this.state.newReview.comment}</h3>
<div>
<h4>Leave a new review</h4>
<textarea ref="comment" type="text" placeholder='Tell us your thoughts '></textarea>
<button onClick={ function(){this.addReview(indBook.id)}.bind(this) } >Submit</button>
</div>
</div>
)}
</div>
</div>
)
}
}
export default EachBook;
Many thanks for your help!
You’re biggest issue is that you’re mutating items in state, so state doesn’t cause a re-render. I would consider using filter instead of splice and just setting items from that like so:
removeItemFromBasket(itemId) {
const items = this.stat.items.filter(item => item.id !== itemId)
this.setState({ items })
}
The addItemToBasket should also have that issue. You should not mutate state as JavaScript passes objects by reference. You should be using this.state.total instead of this.props.total in the render method.
Hope that helps.
I was able to get the url in the attachment field but for the redux form it was empty, how is possible to pass the value of the url to the redux form? Below is the code and the screenshot:
<div className="FileUpload">
<Dropzone
onDrop={this.onImageDrop.bind(this)}
multiple={false}
accept="image/*">
<div>Drop an image or click to select a file to upload.</div>
</Dropzone>
</div>
<div className="form-group">
<label htmlFor="attachment">Attachment:</label><br />
<input className="form-control" focus placeholder="attachment" type="text" name="attachment" ref="attachment" value={this.state.uploadedFileCloudinaryUrl} />
{this.state.uploadedFileCloudinaryUrl === '' ? null :
<div>
<p>{this.state.uploadedFile.name}</p>
<img src={this.state.uploadedFileCloudinaryUrl} alt="" />
</div>}
</div>
<div className="ui small image">
<img src={this.props.workrequest.attachment} alt="" />
</div>
the url in the attachemnt field
The first one is using the React Dropzone to get the url but for the Redux Form it was empty. May I know how to do that to get the url inserts at Redux Form here? Thank you
import React from 'React';
import { connect } from 'react-redux';
import { change, reduxForm } from 'redux-form';
import Dropzone from 'react-dropzone';
class UploadForm extends React.Component {
onDrop = (accepted) => {
if (!accepted.length) return;
// start uploading
this.setState({ isUploading: true });
const formData = new FormData();
formData.append('file', accepted[0]);
axios.post('upload', formData).then(
(res) => {
this.props.dispatch(change('uploadForm', 'url', res.url));
this.setState({ isUploading: false });
},
() => {
this.setState({ isUploading: false });
}
);
};
render() {
return (
<form>
<Dropzone onDrop={this.onDrop} />
</form>
);
}
}
export default connect()(
reduxForm({
form: 'uploadForm',
initialValues: {
url: ''
}
})(UploadForm)
);
Please use this.
Actually, you must use Field component from Redux-form.
Other wise, you can change form values using dispatch and change action creator.
import { Field, change } from 'redux-form';
this.props.dispatch(change('uploadForm', 'url', this.state.url));