I have the following code where inside my React component I'm using a third party component -- FineUploader in my case.
Upon uploading files, it calls its onComplete function. From here, I'm trying to call my action creators to handle post-upload processes but I'm unable to access my props or actions from there because this is all outside of my component.
Up to that point, everything is working. I'm able to call uploader instance from my component and upload the file to my Azure Blob Storage and get the fileName and blobName once the upload is completed.
It's funny that I'm stuck at the easier part!
Here's my component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import FineUploaderAzure from 'fine-uploader-wrappers/azure'
// Components
import Gallery from './gallery/index';
// Actions
import * as myActions from '../myActions';
// Instantiate FineUploader
const uploader = new FineUploaderAzure({
options: {
cors: {
expected: true,
sendCredentials: false
},
signature: {
endpoint: 'http://localhost:123/getsas'
},
request: {
endpoint: 'https://myaccount.blob.core.windows.net/my-container'
},
callbacks: {
onComplete: function (id, name, responseJSON, xhr) {
const fileName = uploader.methods.getName(id);
const blobName = uploader.methods.getBlobName(id);
// I now need to call my action creator to handle backend stuff
// Or I can call the handleFileUpload function inside my component.
// How do I access either my action creator or handleFileUpload function from here?
}
}
}
})
class FileUploader extends Component {
constructor(props) {
super(props);
this.handleFileUpload = this.handleFileUpload.bind(this);
}
handleFileUpload(fileName, blobName) {
debugger;
}
render() {
return (
<Gallery uploader={uploader} />
)
}
}
function mapStateToProps(state, ownProps) {
return {
}
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(myActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader)
I came up with the following approach that works. I'm not sure if this is the best way or there's a more elegant approach. I won't accept my answer as the correct one and let everyone post their comments and votes.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import FineUploaderAzure from 'fine-uploader-wrappers/azure'
// Components
import Gallery from './gallery/index';
// Actions
import * as myActions from '../myActions';
// Instantiate FineUploader
const uploader = new FineUploaderAzure({
options: {
cors: {
expected: true,
sendCredentials: false
},
signature: {
endpoint: 'http://localhost:123/getsas'
},
request: {
endpoint: 'https://myaccount.blob.core.windows.net/my-container'
}
}
})
class FileUploader extends Component {
constructor(props) {
super(props);
this.handleFileUpload = this.handleFileUpload.bind(this);
}
componentDidMount() {
uploader.on('complete', (id, name, responseJSON, xhr) => {
const originalName = uploader.methods.getName(id);
const blobName = uploader.methods.getBlobName(id);
this.handleFileUpload(originalName, blobName);
}
}
handleFileUpload(fileName, blobName) {
// Received fileName and blobName. We can call our actions creators here.
this.props.actions.someAction(fileName, blobName);
}
render() {
return (
<Gallery uploader={uploader} />
)
}
}
function mapStateToProps(state, ownProps) {
return {
}
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(myActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader)
Related
Below is the code of the Service
import { fetchEventSource } from '#microsoft/fetch-event-source';
export const AlertFetchEventSource = () => {
fetchEventSource('https://puppygifs.tumblr.com/api/read/json'),
{
onmessage(ev) {
const data = JSON.parse(ev.data);
return data;
},
};
};
export default { AlertFetchEventSource };
index.tsx where I am making the call to this service but its not returning any data
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
import './style.css';
import backendService from './services/backendService';
interface AppProps {}
interface AppState {
name: string;
}
class App extends Component<AppProps, AppState> {
constructor(props) {
super(props);
this.state = {
name: 'React',
};
console.log(backendService.AlertFetchEventSource());
}
render() {
return (
<div>
<Hello name={this.state.name} />
<p>Start editing to see some magic happen :)</p>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Given the library you're using is an event handler, it doesn't ever actually return data. You'd need to provide it a callback to execute when it receives events.
// you might want to specify a better type than `any`
type MessageHandler = (data: any) => void;
export const AlertFetchEventSource = (onEvent: MessageHandler) => {
fetchEventSource('https://puppygifs.tumblr.com/api/read/json'), {
onmessage(ev) {
const data = JSON.parse(ev.data);
onEvent(data);
},
}); // you had a typo here, missing ")"
};
and use it like
import { AlertFetchEventSource } from "./services/backendService";
// snip
AlertFetchEventSource(data => {
// handle event data, eg
console.log(data);
// or perhaps something like
this.setState(data);
});
I want my component to fetch an array of objects from the server. Each object is a message with author, body and date. I then want to render these messages in my react component.
My react component currently fetches data from the server before mounting. It will then store this message list in the redux state.|
I'm sure there's a better way of writing this code.
1. Can I place the fetch request in either the Action or Reducer file?
2. Can I write a function in the component to make the async call?
import React, { Component } from 'react';
import Message from '../components/message.jsx';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
// Actions
import { fetchMessages } from '../actions/actions_index.js';
class MessageList extends Component {
constructor(props) {
super(props)
}
componentWillMount() {
fetch('https://wagon-chat.herokuapp.com/general/messages')
.then(response => response.json(),
error => console.log('An error occured receiving messages', error))
.then((data) => {
this.props.fetchMessages(data.messages);
});
}
render() {
return (
<div className="message-list">
{this.props.messageList.map( (message, index) => { return <Message key={index} message={message}/> })}
</div>
)
}
}
function mapStateToProps(state) {
return {
messageList: state.messageList
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ fetchMessages: fetchMessages },
dispatch
)
}
export default connect(mapStateToProps, mapDispatchToProps)(MessageList);
Can I place the fetch request in either the Action or Reducer file?
The fetch request should be placed in action creator. Where the retrieved data will be dispatched to reducer later to manipulate the data, and lastly update the store to show on UI. Here's simple flow for most of react-redux app.
UI -> Action creator (calling request, saga etc..) -> reducer -> store -> UI
Can I write a function in the component to make the async call?
Yes, this should be called action creator, and you can see actions.js below for more reference.
I think you can safely follow this sample pattern where most tutorials out there apply. I'm assuming all files listed here are in the same directory.
constant.js
const MESSAGE_FETCH__SUCCESS = 'MESSAGE/FETCH__SUCCESS'
const MESSAGE_FETCH__ERROR = 'MESSAGE/FETCH__ERROR'
export {
MESSAGE_FETCH__SUCCESS,
MESSAGE_FETCH__ERROR
}
actions.js
import {
MESSAGE_FETCH__SUCCESS,
MESSAGE_FETCH__ERROR
} from './constant';
const fetchMessageError = () => ({
type: MESSAGE_FETCH__ERROR
})
const fetchMessageSuccess = data => ({
type: MESSAGE_FETCH__SUCCESS,
payload: data
})
const fetchMessages = () => {
const data = fetch(...);
// if error
if (data.error)
fetchMessageError();
else fetchMessageSuccess(data.data);
}
export {
fetchMessages
}
reducers.js
import {
MESSAGE_FETCH__SUCCESS,
MESSAGE_FETCH__ERROR
} from './constant';
const INIT_STATE = {
messageList: []
}
export default function( state = INIT_STATE, action ) {
switch(action.type) {
case MESSAGE_FETCH__SUCCESS:
return {
...state,
messageList: action.payload
}
case MESSAGE_FETCH__ERROR:
// Do whatever you want here for an error case
return {
...state
}
default:
return state;
}
}
index.js
Please read the comment I noted
import React, { Component } from 'react';
import Message from '../components/message.jsx';
import { connect } from 'react-redux';
// Actions
import { fetchMessages } from './actions';
class MessageList extends Component {
/* If you don't do anything in the constructor, it's okay to remove calling `constructor(props)`
*/
//constructor(props) {
// super(props)
//}
// I usually put this async call in `componentDidMount` method
componentWillMount() {
this.props.fetchMessage();
}
render() {
return (
<div className="message-list">
{
/* Each message should have an unique id so they can be used
for `key` index. Do not use `index` as an value to `key`.
See this useful link for more reference: https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js
*/
this.props.messageList.map( message => <Message key={message.id} message={message}/> )
}
</div>
)
}
}
function mapStateToProps(state) {
return {
messageList: state.messageList
}
}
export default connect(mapStateToProps, {
fetchMessages
})(MessageList);
You could use redux-thunk in an action called getMessages.
So:
(The double arrow func, is to return an action, see redux-thunk)
const getMessages = ()=>(dispatch, getState)=>{
fetch('https://wagon-chat.herokuapp.com/general/messages')
.then(response => response.json(),
error => dispatch(['error', error]))
.then((data) => {
dispatch(data);
})
}
Then you've successfully reduced your component to:
componentWillMount(){
this.props.getMessages()
}
I think #Duc_Hong answered the question.
And in my opinion, I suggest using the side-effect middle-ware to make AJAX call more structured, so that we could handle more complicated scenarios (e.g. cancel the ajax request, multiple request in the same time) and make it more testable.
Here's the code snippet using Redux Saga
// Actions.js
const FOO_FETCH_START = 'FOO\FETCH_START'
function action(type, payload={}) {
return {type, payload};
}
export const startFetch = () => action{FOO_FETCH_START, payload);
// reducer.js
export const foo = (state = {status: 'loading'}, action) => {
switch (action.type) {
case FOO_FETCH_STARTED: {
return _.assign({}, state, {status: 'start fetching', foo: null});
}
case FOO_FETCH_SUCCESS: {
return _.assign({}, state, {status: 'success', foo: action.data});
}
......
}
};
Can I place the fetch request in either the Action or Reducer file?
// Saga.js, I put the ajax call (fetch, axios whatever you want) here.
export function* fetchFoo() {
const response = yield call(fetch, url);
yield put({type: FOO_FETCH_SUCCESS, reponse.data});
}
// This function will be used in `rootSaga()`, it's a listener for the action FOO_FETCH_START
export function* fooSagas() {
yield takeEvery(FOO_FETCH_START, fetchFoo);
}
Can I write a function in the component to make the async call?
// React component, I trigger the fetch by an action creation in componentDidMount
class Foo extends React.Component {
componentDidMount() {
this.props.startFetch();
}
render() {
<div>
{this.props.foo.data ? this.props.foo.data : 'Loading....'}
<div>
}
}
const mapStateToProps = (state) => ({foo: state.foo});
const mapDispatchToProps = { startFetch }
export default connect(mapStateToProps, mapDispatchToProps) (Foo);
//client.js, link up saga, redux, and React Component
const render = App => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
combinedReducers,
initialState,
composeEnhancers(applyMiddleware(sagaMiddleware))
);
store.runSaga(rootSaga);
return ReactDOM.hydrate(
<ReduxProvider store={store}>
<BrowserRouter><AppContainer><App/></AppContainer></BrowserRouter>
</ReduxProvider>,
document.getElementById('root')
);
}
I'm working my way learning some react+redux-thunk and I've put together a simple form that hits an API and retrieves some jokes. My core component and code:
containers/AsyncApp.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {bindActionCreators} from 'redux';
import SearchJokes from '../components/SearchJokes';
import Jokes from '../components/Jokes';
import {fetchJokes} from '../actions';
class AsyncApp extends Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleInput = this.handleInput.bind(this)
this.state = {searchText: ''};
}
handleSubmit(e){
e.preventDefault();
//const {searchText} = this.props;
console.log('button clicked ' + this.state.searchText);
this.props.fetchJokes(this.state.searchText);
}
handleInput = (e) => {
this.setState({
searchText: e.target.value,
})
}
render(){
const { jokes, isFetching } = this.props
return(
<div>
<SearchJokes
handleSubmit={this.handleSubmit}
onChange={this.handleInput}
searchText={this.state.searchText}
/>
{jokes ? (<Jokes jokes={jokes}/>) : (<div></div>)}
</div>
)
}
}
function mapStateToProps(state) {
return {
isFetching: state.isFetching,
jokes: state.items
}
}
function mapDispatchToProps(dispatch) {
return{
fetchJokes: bindActionCreators(fetchJokes, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AsyncApp)
actions/index.js
export const REQUEST_JOKES = 'REQUEST_JOKES'
export const RECEIVE_JOKES = 'RECEIVE_JOKES'
function requestJokes(term) {
return {
type: REQUEST_JOKES,
term
}
}
function receiveJokes(term, json) {
return {
type: RECEIVE_JOKES,
term,
jokes: json.results.map(joke => joke)
}
}
export function fetchJokes(term) {
return dispatch => {
dispatch(requestJokes(term))
return fetch(`https://icanhazdadjoke.com/search?term=${term}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
}).then(response => response.json())
.then(json => dispatch(receiveJokes(term, json)))
}
}
reducers/index.js
import { combineReducers } from 'redux'
import {
REQUEST_JOKES,
RECEIVE_JOKES
} from '../actions'
function jokesBySearch(state = {}, action) {
switch(action.type){
case REQUEST_JOKES:
return Object.assign({}, state, {isFetching: true, items: []})
case RECEIVE_JOKES:
return Object.assign({}, state, {
isFetching: false,
items: action.jokes,
})
default:
return state
}
}
const rootReducer = combineReducers({
jokesBySearch,
})
export default rootReducer
The form component works, and I can see the json array returned as part of the action. But the value of this.props.jokes is empty or undefined in the console.log and I'm wondering how to populate it once the results from the API call are returned.
I see two problems in this code.
First you don't need to use combineReducers when you only have one reducer. Just keep it simple and add complexity only when absolutely necessary. Instead of combineReducers
export default jokesBySearch
If you do use combineReducers be mindful how it affects your state.
Second there is no need to use bindactioncreators here, just dispatch the action (and don't forget to pass the term).
function mapDispatchToProps(dispatch) {
return{
fetchJokes: (term) => dispatch(fetchJokes(term))
}
}
See here when you need to use bindactioncreators (pretty rare use case)
In general good way to lear a library like redux is just use the core functionality until you encounter problem and only then add complexity.
After upgrading to Meteor 1.5 from 1.4, createContainer function from react-meteor-data gives the following error:
Uncaught TypeError: Super expression must either be null or a function, not undefined
at exports.default (modules.js?hash=fb99b6a…:1144)
at ReactMeteorData.jsx:6
at ReactMeteorData.jsx:6
at createContainer (createContainer.jsx:16)
at AppContainer.jsx (AppContainer.jsx:8)
AppContainer.jsx:
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { createContainer } from 'meteor/react-meteor-data';
import App from '../layouts/App.jsx';
export default AppContainer = createContainer(props => {
return {
currentUser: Meteor.user(),
};
}, App);
App file below, in constructor i am performing super(props) however error is still thrown
App.jsx:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
menuOpen: false,
showConnectionIssue: false,
headerTitle: null,
};
this.setHeaderTitle = this.setHeaderTitle.bind(this);
this.logout = this.logout.bind(this);
}
logout() {
Meteor.logout();
this.context.router.replace(`/home`);
}
render() {
... omitted render function
}
}
App.propTypes = {
user: React.PropTypes.object, // current meteor user
connected: React.PropTypes.bool, // server connection status
loading: React.PropTypes.bool, // subscription status
menuOpen: React.PropTypes.bool, // is side menu open?
children: React.PropTypes.element, // matched child route component
location: React.PropTypes.object, // current router location
params: React.PropTypes.object, // parameters of the current route
};
App.contextTypes = {
router: React.PropTypes.object,
};
export default App;
Personnally, for a container data i do like this (from masterchef Base, updated and working as well) :
/* AppContainer.js */
// import react and proptypes ...
import { Meteor } from 'meteor/meteor';
import container from '../../../modules/container';
// Some code for app layout and proptypes...
export default container((props, onData) => {
const user= Meteor.user(); // adapted for your case
onData(null, {
currentUser:user,
// and others data ...
});
}, App);
/* container.js */
import { compose } from 'react-komposer';
import getTrackerLoader from './get-tracker-loader';
export default function container(composer, Component, options = {}) {
return compose(getTrackerLoader(composer), options)(Component);
}
/* get-tracker-loader.js */
import { Tracker } from 'meteor/tracker';
export default function getTrackerLoader(reactiveMapper) {
return (props, onData, env) => {
let trackerCleanup = null;
const handler = Tracker.nonreactive(() => Tracker.autorun(() => {
trackerCleanup = reactiveMapper(props, onData, env);
}));
return () => {
if (typeof trackerCleanup === 'function') trackerCleanup();
return handler.stop();
};
};
}
Hope it ll be useful.
Try following snippet:
export default AppContainer = createContainer((props) => {
// do subscriptions if you have any
return {
currentUser: Meteor.user(),
};
}, App);
You might be missing super() in the App Component.
i am making a simple todo app on react the app was working fine when i store my data in the predefined object.
but now am i getting my data from a link (rest) using ajax , getting problem with this ,
path- pages/todo.js
import React from "react";
import Todo from "../components/Todo";
import * as TodoActions from "../actions/TodoActions.js";
import TodoStore from "../stores/TodoStore";
export default class Settings extends React.Component {
constructor(){
super();
this.getTodos=this.getTodos.bind(this);
this.state={
todos: TodoStore.getAll(),
};
console.log("STATE",this.state.todos);
}
componentDidMount(){
TodoStore.on("load",this.getTodos);
}
getTodos()
{
this.setState({
todos:TodoStore.getAll(),
});
}
reloadTodos(){
TodoActions.reloadTodos();
}
render() {
const {todos}=this.state;
const TodoComponents=todos.map((todo)=>{
return <Todo key={todo.id} {...todo}/>;
});
return (
<div>
<button onClick={this.reloadTodos.bind(this)}>Reload!!</button>
<h1>TODO.net</h1>
<ul>{TodoComponents}</ul>
</div>
)
}
}
path -stores/Todo
import {EventEmitter} from "events";
import * as $ from "jquery";
import dispatcher from "../Dispatcher";
class TodoStore extends EventEmitter
{
constructor(){
super()
this.todos=[];
}
createTodo(text)
{ const id=Date.now();
this.todos.push({
id,
text,
complete:false
});
this.emit("change");
}
getAll(){
$.ajax({
type: 'GET',
url: 'http://rest.learncode.academy/api/chitranks/todo',
success: function(data) {
console.log("here",data);
this.todos=data;
window.todos=this.todos;
}.bind(this),
error: function() {
alert('error GET connecting to REST');
}.bind(this)
});
return this.todos;
}
handleActions(action) {
switch(action.type){
case "CREATE_TODO":{
this.createTodo(action.text);
}
case "RECEIVED_TODOS":{
this.todos=action.todos;
this.emit("change");
}
}
}
}
const todoStore=new TodoStore;
dispatcher.register(todoStore.handleActions.bind(todoStore));
window.dispatcher=dispatcher;
export default todoStore;
when i type in console todos i can see the data, but it is not rendering (shows undefined)
and also in pages/todo.js file the object is undefined
Since AJAX is asynchronous you can't return the actual data from response. Instead use callbacks to obtain data from response.
Here is the updated code
import React from "react";
import Todo from "../components/Todo";
import * as TodoActions from "../actions/TodoActions.js";
import TodoStore from "../stores/TodoStore";
export default class Settings extends React.Component {
constructor(){
super();
this.getTodos=this.getTodos.bind(this);
this.state={
todos: []
};
console.log("STATE",this.state.todos);
}
componentWillMount(){
this.getTodos();//directly hydrating store with todos.
}
getTodos()
{
var _this = this; //context saved to another variable to use this in anonymous function as callback
TodoStore.getAll(function(todos){
_this.setState({
todos: todos,
}, function(){ console.log("updated TODOS::", _this.state.todos)});
});
}
reloadTodos(){
TodoActions.reloadTodos();
}
render() {
const {todos}=this.state;
const TodoComponents=todos.map((todo)=>{
return <Todo key={todo.id} {...todo}/>;
});
return (
<div>
<button onClick={this.reloadTodos.bind(this)}>Reload!!</button>
<h1>TODO.net</h1>
<ul>{TodoComponents}</ul>
</div>
)
}
}
And your store
import {EventEmitter} from "events";
import * as $ from "jquery";
import dispatcher from "../Dispatcher";
class TodoStore extends EventEmitter{
constructor(){
super()
this.todos=[];
}
createTodo(text){
const id=Date.now();
this.todos.push({
id,
text,
complete:false
});
this.emit("change");
}
getAll(callback){ //call this on componentWillMount() (required)
$.ajax({
type: 'GET',
url: 'http://rest.learncode.academy/api/chitranks/todo',
success: function(data) {
console.log("here",data);
this.todos=data; //store gets hydrated with todos
if(typeof callback == "function"){
callback(data) //giving data to callback
}
window.todos=this.todos; //not necessary other than for checking in console
}.bind(this),
error: function() {
alert('error GET connecting to REST');
}.bind(this)
});
return this.todos; //not the proper palce to retutn
}
handleActions(action) {
switch(action.type){
case "CREATE_TODO":{
this.createTodo(action.text);
}
case "RECEIVED_TODOS":{
this.todos=action.todos;
this.emit("change");
}
}
}
}
const todoStore=new TodoStore;
dispatcher.register(todoStore.handleActions.bind(todoStore));
window.dispatcher=dispatcher;
export default todoStore;