I've implemented FineUploader in my React app to upload files to my Azure Blob Storage and it's working fine except for one issue.
After uploading a file successfully, if I try uploading another one, FineUploader is not allowing me to select a new file. Clicking the select button opens up the dialog box to let me select a file but clicking the file to select it simply does nothing. I don't see any errors either.
Any idea what could be causing this issue?
Here's my implementation of FineUploader in my app:
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 fileActions from '../../../../actions/file-actions';
// Instanciate FineUploader
const uploader = new FineUploaderAzure({
options: {
cors: {
expected: true,
sendCredentials: false
},
signature: {
endpoint: 'https://api.myapp.com/files/get/sas'
},
request: {
endpoint: 'https://myaccount.blob.core.windows.net/my-container'
},
validation: {
itemLimit: 1
}
}
})
class FileUploader extends Component {
constructor(props) {
super(props);
this.saveFileInfo = this.saveFileInfo.bind(this);
}
componentDidMount() {
uploader.on('complete', (id, name, responseJSON, xhr) => {
const originalName = uploader.methods.getName(id);
const blobName = uploader.methods.getBlobName(id);
const fileSize = uploader.methods.getSize(id);
this.saveFileInfo(originalName, blobName, fileSize);
})
}
saveFileInfo(fileName, blobName, fileSize) {
// Gather necessary information
const accountId = this.props.accountId;
const id = this.props.id;
const folderId = this.props.activeFolder.id;
const files = [
{
fileName: blobName,
displayName: fileName,
fileSize: fileSize
}
];
// Call backend API to save file info in database
this.props.actions.createFileRecords(accountId, bizObject, id, privilegeLevel, folderId, files);
// Close modal
const modalId = this.props.modalId;
return this.props.handleClose(modalId, false);
}
render() {
return (
<div style={{ position: 'fixed', zIndex: 250000990 }}>
<div className="modal-wrapper">
<div className="height-100 width-100" style={{ background: 'transparent', position: 'absolute', left: '0', top: '0' }}></div>
<div className="modal-window vertical-center">
<div className="modal-controls padding-right-20 padding-top-10" style={{ height: '50px', position: 'absolute', right: '0', top: '0', lineHeight: '40px', borderColor: 'transparent !important' }}>
<a className="modal-control mc-help"></a>
<a className="modal-control mc-close" onClick={e => this.props.handleClose(this.props.modalId, false)}></a>
</div>
<div className="width-100 height-100 border-radius border-black-w1-1" style={{ overflow: 'hidden', background: 'black !important', borderColor: 'black' }}>
<Gallery uploader={uploader} onComplete={this.handleFileUpload} />
</div>
<div style={{ position: 'absolute', bottom: '17px', left: '17px' }}>
{/* Privilege Level Selector */}
{this.renderPrivilegeLevels()}
<span className="app-btn app-btn-lg margin-left-20">Uploading into Folder: <strong>{this.props.activeFolder.name}</strong></span>
</div>
</div>
</div>
</div>
)
}
}
function mapStateToProps(state, ownProps) {
return {
modalId: ownProps.modalId,
accountId: ownProps.accountId,
id: ownProps.id,
folders: ownProps.folders,
activeFolder: ownProps.activeFolder,
fileUpload: state.files.fileUpload,
errors: state.files.errors,
handleClose: ownProps.handleClose
}
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(fileActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader)
As explained in the comment the itemLimit: 1 option is limiting the total number of upload for that Uploader to 1.
Since what you are trying to achieve is actually avoiding multiple upload at once you can use the option multiple: false to block selection of multiple files.
Also, to avoid users adding more files while others are still uploading, you can use a custom validation where you check if some other files are in progress, for example:
options: {
..., //other options
callbacks: {
onValidate: function (file) {
if(getInProgress() > 0){
return false;
}
return true;
}
}
Note that the onValidate events is called before the default Fine Uploader validators
Related
I wanted to add an video call feature with WebRTC into my chat webapp. So after writing the code i tested the code. -> No video and in the console the error "Uncaught (in promise) TypeError: Cannot set properties of null (setting 'srcObject')"
screenshot error message
My code:
//CallProvider
import React, { createContext, useState, useRef, useEffect } from 'react';
import Peer from 'simple-peer';
import io from 'socket.io-client';
export const VideoCallContext = createContext();
const socket = io('http://localhost:5001');
export function CallProvider({ conv, id, children }) {
const videoGrid = document.querySelector('#video-grid');
const [stream, setStream] = useState(null);
const myVideoRef = useRef(null);
const peer = new Peer({
trickle: false,
stream,
});
const peers = {};
useEffect(() => {
navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then((currentStream) => {
setStream(currentStream);
myVideoRef.current.srcObject = currentStream.streams[0];
});
socket.on('user-connected', (userId) => {
connectToNewUser(userId, stream);
});
});
socket.on('user-disconnected', (userId) => {
if (peers[userId]) peers[userId].close();
});
function joinCall() {
peer.on('signal', (data) => {
socket.emit('join-call', {
call: conv,
userId: id,
});
});
}
function leaveCall() {
socket.emit('leave-call', {
call: conv,
userId: id,
});
navigator.mediaDevices.getUserMedia({
video: false,
audio: false,
});
}
function connectToNewUser(userId, stream) {
const call = peer.call(userId, stream);
const video = document.createElement('video');
call.on('stream', (userVideoStream) => {
addVideoStream(video, userVideoStream);
});
call.on('close', () => {
video.remove();
});
}
function addVideoStream(video, stream) {
video.srcObject = stream;
video.addEventListener('loadedmetadata', () => {
video.play();
video.playsInline = true;
});
videoGrid.append(video);
}
const value = {
stream,
myVideoRef,
joinCall,
leaveCall,
};
return (
<VideoCallContext.Provider value={value}>
{children}
</VideoCallContext.Provider>
);
}
// FormCall
//use WebRTC from simplepeer
import React, { useContext } from 'react';
import { Button } from 'react-bootstrap';
import { VideoCallContext } from '../contexts/CallProvider';
export default function FormCall({ id, conv, closeWindow }) {
const { stream, myVideoRef, joinCall, leaveCall } =
useContext(VideoCallContext);
const conversation = conv;
function closeCall() {
leaveCall(id);
closeWindow();
}
return (
<>
<style>
{`
#video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(210px, 230px));
grid-auto-rows: auto;
grid-gap: 5px;
}
`}
{`
video {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
object-position: center;
border-radius: 10px;
overflow: hidden;
}
`}
</style>
<div className="pb-2" id="video-grid">
<video playsInline muted ref={myVideoRef} autoPlay></video>
</div>
<div className="d-flex flex-row justify-content-around border-top pt-2">
<Button
onClick={closeCall}
className="rounded-circle position-relative"
style={{ height: '40px', width: '40px' }}
>
<i
className="bi bi-telephone-x-fill position-absolute"
style={{ left: '28%', top: '20%' }}
/>
</Button>
</div>
</>
);
}
My installed packages are:
packages
I have looked for an solution for the error. Nothing that is a solution for my problem.
I also have changed the "currentStream" to "currentStream.streams[0]", nothing changed.
When you set const myVideoRef = useRef(null)), you're setting myVideoRef.current to null.
If you need to the set srcObject property in your ref:
const myVideoRef = useRef({ srcObject: null }).
Now myVideoRef.current.srcObject exists and can be set.
Try
const value = {
stream:stream,
myVideoRef:myVideoRef,
joinCall:joinCall,
leaveCall:leaveCall,
};
Also i would avoid returning *.Provider itself.
Wrap CallProvider and it's children like this:
<VideoCallContext.Provider value={Your value}>
<CallProvider >
<Child1 />
<Child2 />
</CallProvider>
</VideoCallContext.Provider>
If You mreturn instance of Context.Provider as Component, then every state mutation rerenders Your Context . The Context is innescesarry then.
If Your Context fully wraps dependant DOM snippet, then Your Context values are stable.
At least one of these two has to be changed to solve Your problem.
P.S.
As Wesley LeMahieu found ,hence i would use useRef() instead of useRef(null).
I had never used React Class Components And Want to shoot confetti when some event happened. I'm confused with Class Component. Hope You can help with this issue. tried by creating arrow functions and removing this keyword .But I can't understand how to transform getInstance() function even why it is there?
import React, { Component } from 'react';
import ReactCanvasConfetti from 'react-canvas-confetti';
export default class Confetti extends Component {
getInstance = (instance) => {
// saving the instance to an internal property
this.confetti = instance;
}
onClickDefault = () => {
// starting the animation
this.confetti();
}
onClickCustom = () => {
// starting the animation with custom settings
this.confetti({ particleCount: Math.ceil(Math.random() * 1000), spread: 180 });
}
onClickCallback = () => {
// calling console.log after the animation ends
this.confetti().then(() => {
console.log('do something after animation');
});
}
onClickReset = () => {
// cleaning the canvas
this.confetti.reset();
}
render() {
const style = {
position: 'fixed',
width: '100%',
height: '100%',
zIndex: -1
};
const stylediv = {
position: 'fixed',
width: '100%',
height: '100%',
zIndex: 300000
};
return (
<>
<div style={stylediv}>
<ReactCanvasConfetti
// set the styles as for a usual react component
style={style}
// set the class name as for a usual react component
className={'yourClassName'}
// set the callback for getting instance. The callback will be called after initialization ReactCanvasConfetti component
refConfetti={this.getInstance}
/>
<button onClick={this.onClickDefault}>Fire with default</button>
<button onClick={this.onClickCustom}>Fire with custom</button>
<button onClick={this.onClickCallback}>Fire with callback</button>
<button onClick={this.onClickReset}>Reset</button>
</div>
</>
);
}
}
I'm trying to create Functional Component of the above Class Component
import React, { useRef } from 'react';
import ReactCanvasConfetti from 'react-canvas-confetti';
const Confetti = () => {
const Ref = useRef()
const getInstance = (instance) => {
if (Ref.current) {
Ref.current.confetti = instance
}
}
const onClickDefault = () => {
Ref.current.confetti();
}
const onClickCustom = () => {
Ref.current.confetti({ particleCount: Math.ceil(Math.random() * 1000),
spread: 180 });
}
const onClickCallback = () => {
Ref.current.confetti().then(() => {
console.log('do something after animation');
});
}
const onClickReset = () => {
Ref.current.confetti.reset();
}
const style = {
position: 'fixed',
width: '100%',
height: '100%',
zIndex: -1
};
const stylediv = {
position: 'fixed',
width: '100%',
height: '100%',
zIndex: 300000
};
return (
<>
<div ref={Ref} style={stylediv}>
<ReactCanvasConfetti
style={style}
className={'yourClassName'}
refConfetti={getInstance}
/>
<button onClick={onClickDefault}>Fire with default</button>
<button onClick={onClickCustom}>Fire with custom</button>
<button onClick={onClickCallback}>Fire with callback</button>
<button onClick={onClickReset}>Reset</button>
</div>
</>
);
}
export default Confetti
I built a card component that shows a list of user data and images with antd on nextJs. I want to build a functionality that creates a modal to input new data and image and adds it to the user interface as a new card, but I am confused on how to get my hands around it. I need assistance. Here's a link to my code!
import React from 'react';
import { Avatar, Card, Icon, List } from 'antd';
import { ICON_LIST, LIST_TEXTS, STYLES, USER_UPLOAD } from './constants';
const { AVATAR, CARD_CONTAINER, ICON, USER_LIST } = STYLES;
const { INNER, MORE, UPLOAD, VERTICAL } = LIST_TEXTS
class Home extends React.Component {
state = {
clicks: 0,
};
IncrementIconText = () => {
this.setState({ clicks: this.state.clicks + 1 });
}
render() {
const actions = ( ICON_LIST.map(({ type }) => (
<span>
<Icon key={type} type={type} onClick={this.IncrementIconText} style={ICON} />
{this.state.clicks}
</span>
)));
return (
<List
itemLayout={VERTICAL}
dataSource={USER_UPLOAD}
renderItem={item => (
<List.Item style={USER_LIST}>
<Card
actions={actions}
cover={<img alt={UPLOAD} src={item.image} />}
extra={<Icon type={MORE} />}
hoverable
title={<a><Avatar src={item.image} style={AVATAR} />{item.user}</a>}
type={INNER}
style={CARD_CONTAINER}
>
{item.story}
</Card>
</List.Item>
)}
/>
);
}
}
export default Home;
constants.js
export const ICON_LIST = [
{
key: "heart",
type: "heart",
},
{
key: "dislike",
type: "dislike",
},
{
key: "meh",
type: "meh",
},
]
export const LIST_TEXTS = {
INNER: "inner",
MORE: "more",
UPLOAD: "upload",
VERTICAL: "vertical",
};
export const STYLES = {
AVATAR: {
marginRight: 8
},
CARD_CONTAINER: {
width: "650px",
marginBottom: 50
},
ICON: {
marginRight: 8
},
USER_LIST: {
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center"
},
};
export const USER_UPLOAD = [
{
image: "http://sugarweddings.com/files/styles/width-640/public/1.%20The%20Full%20Ankara%20Ball%20Wedding%20Gown%20#therealrhonkefella.PNG",
story: "Today's my birthday next week! What do you think?",
user: "Chioma",
},
{
image: "https://dailymedia.com.ng/wp-content/uploads/2018/10/7915550_img20181007141132_jpeg01c125e1588ffeee95a6f121c35cd378-1.jpg",
story: "Going for an event. Do you like my outfit",
user: "Simpcy",
},
{
image: "https://i0.wp.com/www.od9jastyles.com/wp-content/uploads/2018/01/ankara-styles-ankara-styles-gown-ankara-tops-ankara-gowns-ankara-styles-pictures-latest-ankara-style-2018-latest-ankara-styles-ankara-ankara-styles.png?fit=437%2C544&ssl=1",
story: "Saturdays are for weddings. Yay or nay!",
user: "Angela",
},
]
So this could get you started:
https://codesandbox.io/s/1r7j6lom34?fontsize=14
I moved your static USER_UPLOAD into the state of Home and wrote a method to add a new upload to that state.
You would now need to come up with a component that shows your modal and calls AddUpload with the right values.
Also your card-actions don't seem to function properly. To fix that i suggest creating a wrapper component for Card that has a state with the appropriate click counters. That way every card has its own clickcounters.
I need some help trying to figure how to use hls.js in react.
Let me explain the situation I have to fetch the m3u8 from an api I'm able to make it work from a basic html with the <script> tag but when i try to implement it on react it doesn't work any help is appreciated. This is what I've got so far:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import Paper from "#material-ui/core/Paper";
import Typography from "#material-ui/core/Typography";
import Grid from "#material-ui/core/Grid";
import ButtonBase from "#material-ui/core/ButtonBase";
import CircularProgress from "#material-ui/core/CircularProgress";
import Hls from "hls.js";
const styles = theme => ({
root: {
flexGrow: 1,
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2,
marginBottom: 24,
marginLeft: 24,
marginRight: 60
},
image: {
marginLeft: 24,
width: 200,
height: 200
},
img: {
display: "block",
width: 200,
height: 200,
maxWidth: "100%",
maxHeight: "100%"
},
detail: {
marginLeft: 16
},
progress: {
display: "flex",
justifyContent: "center",
alignItems: "center"
}
});
class Video extends Component {
constructor(props) {
super(props);
}
componentWillReceiveProps(props) {
if (props.episode && this.player) {
var hlsUrl = props.episode.assets.hls;
var video = this.player;
if (video.canPlayType("application/vnd.apple.mpegurl")) {
// If HLS is natively supported, let the browser do the work!
video.src = "hlsUrl";
video.addEventListener("loadedmetadata", function() {
video.play();
});
} else if (Hls.isSupported()) {
// If the browser supports MSE, use hls.js to play the video
var hls = new Hls({
// This configuration is required to insure that only the
// viewer can access the content by sending a session cookie
// to api.video service
xhrSetup: function(xhr, url) {
xhr.withCredentials = true;
}
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
} else {
alert("Please use a modern browser to play the video");
}
}
}
handleSerieClick = () => {
this.props.history.push("/" + this.props.serie.apiName);
};
_onTouchInsidePlayer() {
if (this.player.paused) {
this.player.play();
} else {
this.player.pause();
}
}
render() {
const { classes, theme } = this.props;
if (this.props.episode) {
const { assets, title, description, videoId } = this.props.episode;
return (
<Grid className={classes.root} item xs={12}>
<video
controls
onClick={this._onTouchInsidePlayer}
ref={player => (this.player = player)}
autoPlay={true}
/>
</Grid>
);
} else {
return (
<Grid className={classes.progress} item xs={12}>
<CircularProgress size={100} />
</Grid>
);
}
}
}
export default withStyles(styles, { withTheme: true })(Video);
This is the code that works with <script> tag
<script src="https://cdn.jsdelivr.net/npm/hls.js#latest"></script>
<video id="video"></video>
<script>
var hlsUrl = 'https://cdn.libcast.net/stream/3de8ff01-18f7-4262-a1f2-abeeb9bb962b/hls/manifest.hls';
var video = document.getElementById('video');
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// If HLS is natively supported, let the browser do the work!
video.src = 'hlsUrl';
video.addEventListener('loadedmetadata',function() {
video.play();
});
} else if (Hls.isSupported()) {
// If the browser supports MSE, use hls.js to play the video
var hls = new Hls({
// This configuration is required to insure that only the
// viewer can access the content by sending a session cookie
// to api.video service
xhrSetup: function(xhr, url) { xhr.withCredentials = true; }
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
} else {
alert('Please use a modern browser to play the video');
}
</script>
I pass the hls source from a parent component in the props and in componentWillRecieveProps i try to use the source to run the player
EDIT
THe problem seems to be that <video> tag is undefined when I try to apply the source.
Initing hls in componentWillReceiveProps is probably "too early". Refs are created during the render execution so your this.video is probably null at that time.
Try moving your logic into componentDidMount (if you pass proper props from the beginnig) or at least componentDidUpdate.
So, I have a function, which convert images to base64. This function is asynchronous, and it converts 4 images with the help of Promise.all(), and then I return object with received strings. So, I export async function. Here is the code:
import IMAC from '../assets/Images/devices/mac_monitor.png';
import MACBOOK from '../assets/Images/devices/macbook_pro.png';
import IPHONE_8 from '../assets/Images/devices/iphone_8.png';
import MSI_LAPTOP from '../assets/Images/devices/msi_laptop.png';
function loadImage(img) {
return new Promise((resolve, reject) => {
toDataURL(img, function (dataUrl) {
resolve(dataUrl);
})
});
}
function toDataURL(url, callback) {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
let reader = new FileReader();
reader.onloadend = function () {
callback(reader.result);
};
reader.readAsDataURL(xhr.response);
};
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
}
const IMAC_IMG_BASE64 = loadImage(IMAC);
const MACBOOK_IMG_BASE64 = loadImage(MACBOOK);
const MSI_IMG_BASE64 = loadImage(MSI_LAPTOP);
const PHONE_IMG_BASE64 = loadImage(IPHONE_8);
export async function loadAllImages() {
const result = await Promise.all([IMAC_IMG_BASE64, MACBOOK_IMG_BASE64, MSI_IMG_BASE64, PHONE_IMG_BASE64]);
return [
{
id: 0,
device: "Apple iMac",
image: result[0],
styles: {
carousel_item: {
width: "41.6vw",
height: "auto",
top: "-4.095vw",
left: "-0.13vw"
},
carousel: {
height: "38vw",
margin: "50px 0"
},
device: {
width: "46.5vw",
height: "38vw",
marginLeft: "-23.25vw"
}
}
},
{
id: 1,
device: "Apple Macbook Pro",
image: result[1],
styles: {
carousel_item: {
width: "37vw",
height: "auto",
top: "-4.4vw",
left: ".6vw"
},
carousel: {
height: "38vw",
margin: "50px 0"
},
device: {
width: "55vw",
height: "30vw",
marginLeft: "-27.5vw"
}
}
},
{
id: 2,
device: "MSI GP72VR 7RFX",
image: result[2],
styles: {
carousel_item: {
width: "35vw",
height: "auto",
top: "-5.8vw",
left: ".5vw"
},
carousel: {
height: "38vw",
margin: "50px 0"
},
device: {
width: "50vw",
height: "34vw",
marginLeft: "-25vw"
}
}
},
{
id: 3,
device: "Iphone 8",
image: result[3],
styles: {
carousel_item: {
width: "14vw",
height: "auto",
top: "-8.2vw",
left: "0"
},
carousel: {
height: "38vw",
margin: "50px 0"
},
device: {
width: "17.7vw",
height: "34vw",
marginLeft: "-8.85vw"
}
}
},
];
}
Then, I have this action creator, which is async, where I received data from this function (loadAllImages()), and then I call dispatch (p.s. - I am using redux-thunk)
export const loadConfigs = () => async dispatch => {
const data = await loadAllImages();
dispatch({type: "LOAD_DATA", payload: data});
};
Also, I have reducer, where I return payload with the object, received from the called dispatch
export default (sliderConfig = null, action) => {
const {type, payload} = action;
switch(type){
case "LOAD_DATA":
return payload;
}
return sliderConfig;
}
Inside the main container App.js, I call this AC inside the componentDidMount()
(Don't look at fetchUser(), it does not matter in this context)
componentDidMount() {
this.props.fetchUser();
this.props.loadConfigs();
}
And, then I have component, where I am using this data, which asynchronously received from the AC. (Do not look at appDesign(), it does not matter in this context)
import React, {Component, PureComponent} from 'react';
import appDesign from '../../../decorators/scroll_resize_decorator';
import Slider from './Slider';
import {connect} from 'react-redux';
import * as actions from '../../../actions';
//Hint: Use container for the images in the slider
//Because errors with movement is appeared
class BlockFour extends Component {
render() {
if (this.props.sliderElements) {
const {sliderElements, width, config, selectConfig} = this.props;
return (
<div className="blockfive">
<div className="blockfive--inner">
<div className="blockfive__container">
<div className="blockfive__container__header">
<div className="blockfive__container__header__container">
<h1>Application Gallery</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
A aliquid blanditiis consequuntur debitis deserunt eaque eligendi
</p>
<div className="blockfive__header--divider"></div>
</div>
</div>
<div className="blockfive__container__device">
<h2>
Choose your device to what screenshots
</h2>
<ul className="tabs">
{
sliderElements.map(item =>
<li
key={item.id}
className="tab"
>
<a href="#"
onClick={
() => selectConfig(item.id)
}
>
{item.device}
</a>
</li>
)
}
</ul>
</div>
<div className="blockfive__container__gallery">
{
<Slider
width={width}
styles={sliderElements[config].styles}
device_image={sliderElements[config].image}
/>
}
</div>
</div>
</div>
</div>
);
}
return null
}
}
const mapStateToProps = ({sliderElements, config}) => {
return {
sliderElements,
config
}
};
export default connect(mapStateToProps, actions)(appDesign(BlockFour));
So, this syntax is working, everything is loading and working. So, I have a question: What is the right way to fetch async data in AC, then pass them to reducer and then load inside the component. I don't want to use if statement inside my component.
I read couple of guides about async/await AC and how use them, but I do not completely understand how to use it in my situation. Could you please give me a brief direction how to implement it here. Thank you!
I personally and most people follow this approach. Its completely personal and wont change much in your app, but might make your life easier.
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
This way your UI and other parts of your app connected to the store can act accordingly depending on the state.
Exemples includes : showing a loading icon or message when FETCH_SMTH_REQUEST is fired and your state changes to fetching and showing an error when FETCH_SMTH_FAILURE and you get the error in your state.
I personally prefer to use constants with the same name of the content, as a Type. Like this:
export const Types = {
FETCH_DATA_START: "FETCH_DATA_START",
FETCH_DATA_SUCCESS: "FETCH_DATA_SUCCESS",
FETCH_DATA_FAIL: "FETCH_DATA_FAIL",
};
Just to be clear, I've hadn't seen nothing wrong and different of it in other projects that I've worked on. At least in my opinion, your action is great. I probably would wrap it all in a try...catch clause to have a best control of my flux of data.
Just to know, if you need, when you using redux-thunk, you have the actual state as a second parameter and, if you need, you can pass extra arguments in the middleware configuration as the third parameter, like an API for exemple. So, your code, could seem like this:
export const fetchMySpecialData = () => async(dispatch, getState, API) => {
try {
dispatch({type: "FETCH_DATA_START"});
const data = await API.fetchData();
dispatch({type: "FETCH_DATA_SUCCESS", payload: data});
} catch (err) {
dispatch({type: "FETCH_DATA_FAIL", error: err});
}
};